學習OpenCL的一些心得

這篇文章記錄我對於前一週學習OpenCL的心得和對於一些東西的理解。


1.GPU

  對於最開始,完全不清楚GPU是個啥,感覺好像很神祕似得,只知道需要話好幾千塊RMB買一個的。所以在一段時間裏面都沒有去碰OpenCL,認爲是沒有GPU,無法學習並測試。直到後來才發現,所謂GPU就是平常所謂的顯卡,帶計算功能的顯卡(以前有的顯卡不支持計算,現在看nVIDIA官網說,他們的所有卡都實現了對cuda的支持)。對於查看顯卡是否支持OpenCL運算,可以下載一個GPU-Z軟件,打開後可以在下面看到,如果支持OpenCL,那麼下面會打個勾勾。




2.OpenCL

  讓OpenCL跑起來很簡單,也就那麼幾句話,只是優化它則需要知道更多的細節。

SDK:我嘗試過Cuda Toolkit裏面的OpenCL和Amd App裏面的OpenCL,發現寫的代碼可以通用(當然,OpenCL的目的也就是跨平臺的),只是一些小的細節上有點區別,比如有的錯誤代碼在Amd App裏面有而在Cuda Toolkit裏面沒有,之類的不太影響使用。所以當然也就可以下載Cuda Toolkit或者Amd App了。

 

  讓OpenCL運行起來

a.這裏貼一個後面需要用到的工具 ocl_Info,它是查詢機器上OpenCL設備信息的東西,將它下載下來並編譯出來,在控制檯中:

                        c:\User\Administrator>ocl_info >1.txt

  然後可以在C:/User/Administrator/目錄下找到1.txt,裏面記錄了設備信息,如下圖,如果沒有找到,軟件會提示的:


Number of platforms: 1   表示現在有1個平臺

Number of devices: 2      表示有2個設備

在OpenCL中,它規定有4種模型(平臺模型,執行模型,內存模型,編程模型),當然只需要瞭解平臺模型就好了,其他暫時放一邊。其中平臺模型是指:

        一臺電腦有多個平臺,一個平臺有多個設備(比如:1個GPU + 1個CPU),一個設備又有多個計算單元,一個計算單元又有多個處理單元(就是所謂的SIMD,可以理解成一個線程)和一塊屬於計算單元自己的局部內存。 知道這些就夠了,然後下面開始是代碼。


b.代碼:

Simple test.txt

__kernel void SimpleTest(__global uint* indata, __global uint* outdata)
{
	uint idx = get_global_id(0);
	outdata[idx] = indata[idx] * 2;
}

main.cpp

#include <stdlib.h>
#include <stdio.h>
#include <cl.h>
#pragma comment(lib, "OpenCL.lib")

char* readFile(const char* file, size_t* len)
{
	FILE* f = fopen(file, "rb");
	if(f == 0) return 0;

	fseek(f, 0, SEEK_END);
	*len = ftell(f);
	fseek(f, 0, SEEK_SET);

	char* data = new char[*len];
	fread(data, 1, *len, f);
	fclose(f);
	return data;
}

int main(int argc, char** argv)
{
	cl_uint uarr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	cl_uint uout[sizeof(uarr) / sizeof(uarr[0])] = {0};
	cl_uint status;
	cl_platform_id platform;
	status = clGetPlatformIDs(1, &platform, 0);

	cl_device_id device;
	clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, 0);

	cl_context context = clCreateContext(0, 1, &device, 0, 0, 0);
	cl_command_queue queue = clCreateCommandQueue(context, device, CL_QUEUE_PROFILING_ENABLE, 0);

	cl_mem clArrBuffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, sizeof(uarr), uarr, 0);
	cl_mem clArrOutBuffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, sizeof(uout), uout, 0);
	size_t len = 0;
	char* code = readFile("Simple test.txt", &len);
	if(code == 0) 
	{
		printf("Can not open \"Simple test.txt\"\n");
		return 0;
	}

	cl_program program = clCreateProgramWithSource(context, 1, (const char**)&code, &len, 0);
	delete code, code = 0;

	status = clBuildProgram(program, 1, &device, 0, 0, 0);
	if (status != CL_SUCCESS)
	{
		printf("Build failed: %d\n", status);
		char tbuf[1024];
		clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 1024, tbuf, NULL);
		printf("\n%s\n", tbuf);
		return -1;
	}

	cl_uint sizeOfArr = sizeof(uarr) / sizeof(uarr[0]);
	cl_kernel kernel = clCreateKernel(program, "SimpleTest", 0);
	clSetKernelArg(kernel, 0, sizeof(cl_mem), (void*)&clArrBuffer);
	clSetKernelArg(kernel, 1, sizeof(cl_mem), (void*)&clArrOutBuffer);
	clSetKernelArg(kernel, 2, sizeof(cl_uint), (void*)&sizeOfArr);

	size_t global_work_size = sizeOfArr;
	clEnqueueNDRangeKernel(queue, kernel, 1, 0, &global_work_size, 0, 0, 0, 0);
	clEnqueueReadBuffer(queue, clArrOutBuffer, CL_TRUE, 0, sizeof(uout), uout, 0, 0, 0);

	clReleaseMemObject(clArrOutBuffer);
	clReleaseMemObject(clArrBuffer);
	clReleaseKernel(kernel);
	clReleaseCommandQueue(queue);
	clReleaseContext(context);

	for (int i = 0; i < sizeof(uout) / sizeof(uout[0]); ++i)
		printf("%d * 2 = %d\n", i + 1, uout[i]);
	
	system("pause");
	return 0;
}


當然,這裏是直接使用第0個平臺第0個設備,如果第0個平臺中沒有GPU,程序會出錯。對於你想選擇第幾個GPU來執行,直接在上面那個ocl_Info導出的列表中看就好了,執行結果就是:



c.使用的函數,列舉下,這個程序中無非就是使用了那麼幾個函數而已:

clGetPlatformIDs---------------------------獲取平臺ID

clGetDeviceIDs-----------------------------獲取設備ID

clCreateContext----------------------------創建上下文

clCreateCommandQueue--------------創建命令隊列

clCreateBuffer------------------------------創建設備內存

clCreateProgramWithSource----------創建程序

clBuildProgram-----------------------------編譯程序

clGetProgramBuildInfo-------------------獲取編譯信息

clCreateKernel-----------------------------創建核

clSetKernelArg-----------------------------設置核參數

clEnqueueNDRangeKernel------------執行核

clEnqueueReadBuffer-------------------讀取設備內存

clReleaseMemObject--------------------釋放內存對象

clReleaseKernel---------------------------釋放核

clReleaseCommandQueue------------釋放命令隊列

clReleaseContext--------------------------釋放上下文

對於各個函數的參數是什麼意思,建議看這個網站(這個是clGetPlatformIDs函數的,查其他函數信息,直接修改末尾爲xxx.html就好了)

http://www.khronos.org/registry/cl/sdk/1.0/docs/man/xhtml/clGetPlatformIDs.html

這裏,着重說明幾個函數:

clCreateKernel:所謂Kernel(核),可以看成是函數,一個Kernel對應一個函數,不同的設備,需要分別創建Kernel。Kernel編譯後可以重複使用。


clEnqueueNDRangeKernel:執行Kernel。它可以同步,也可以異步執行。對於異步,看其最後的3個參數說明就好了。當程序在GPU中執行完成後需要Copy結果到內存,就使用clEnqueueReadBuffer函數,當然也有對應的clEnqueueWriteBuffer函數寫內存。


clCreateBuffer:創建設備內存,也可以說是分配設備內存,這裏分配的是全局內存。在這裏,每次對設備分配內存的最大尺寸是這個設備信息中CL_DEVICE_MAX_MEM_ALLOC_SIZE指定的值,對一個設備允許分配內存的總最大尺寸是CL_DEVICE_GLOBAL_MEM_SIZE指定,單位都是字節。這些值可以在ocl_Info導出的設備信息中找到。分配失敗後其最後一個參數爲錯誤代碼的返回值。這裏需要注意一點是,我測試發現,即使分配尺寸超過最大值,有時它的返回值依然是CL_SUCCESS,但是到clEnqueueReadBuffer函數的時候卻出錯(CL_OUT_OF_RESOURCES),所以分配的時候不要超過最大值了。




對於編寫OpenCL中用的程序,其結構是類C語言,如下:

__kernel void SimpleTest(__global uint* indata, __global uint* outdata)
{
uint idx = get_global_id(0);
outdata[idx] = indata[idx] * 2;
}

1.每個程序返回值必定是void,函數前面必定有前綴__kernel,__global描述符說明指針是來自於公用內存(OpenCL中還有私有內存和局部內存的概念)。

2.get_global_id獲取線程索引,其取值範圍是0 到 global_work_size - 1,如下面,通常global_work_size的取值爲並行任務數。

size_t global_work_size = sizeOfArr;
clEnqueueNDRangeKernel(queue, kernel, 1, 0, &global_work_size, 0, 0, 0, 0);



最後是參考資料:

百度文庫裏面的OpenCL資料:http://wenku.baidu.com/view/53e216a4b0717fd5360cdc24.html

書籍:OpenCL編程指南         --------     對於詳細的東西可以在這個上面找到

發佈了17 篇原創文章 · 獲贊 5 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章