這篇文章記錄我對於前一週學習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;
}
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編程指南 -------- 對於詳細的東西可以在這個上面找到