OpenCL使用GPU滤波
最近开始研究OpenCL,以便在需要大量计算时,用GPU来加速。
为了实用性,结合工作,编写第一个OpenCL的程序。
为了实用,我选择以前做的数字滤波来作为本次的设计目标。我们的数字滤波,主要是用FIR滤波,需要滤波的数据是多通道的电生理数据,滤波系数是使用Matlab仿真的,原始数据是通过多道生理记录仪采集的数据。以前的滤波程序是在CPU上完成的。现在把它移到GPU上完成,就当是一次实验和学习OpenCL的机会。
第一步,下载CUDA(因为我用的是NVidia的显卡,如果使用ATI的显卡,需要下载AMD APP(其前身是 ATI Stream),目前版本2.7,可到网上搜索下载)
CUDA 下载 (版本4.2)
http://developer.nvidia.com/cuda/cuda-downloads
下载完后安装。安装就不介绍了。注意安装的路径,下面要用到。
第二步 VS2008设置
工具->选项
项目->属性
可能你的安装目录跟我的不一样,设置成你的安装目录就可以了。
其它设置可以在网上查。或者 百度文库
第三步,开始编程。
为了实用和可重用,做成一个C++类。这个类必须包含3个函数,InitCL初始化, Uninit释放, Filter_GPU滤波,Filter_GPU可能会反复调用。
首先是初始化CL
//初始化OpenCL
BOOL CFilter::InitCL()
{
if(m_bInitCL)
{
return FALSE;
}
cl_int iError = 0; // 错误代码
// Platform
iError = clGetPlatformIDs(1,&m_clPlatform_id,NULL);
if (iError != CL_SUCCESS)
{
TRACE("Error getting platform id \n");
exit(iError);
}
// Device
iError = clGetDeviceIDs(m_clPlatform_id, CL_DEVICE_TYPE_GPU, 1, &m_clDevice_id, NULL);
if (iError != CL_SUCCESS)
{
TRACE("Error getting device ids \n");
exit(iError);
}
// Context
m_clContext = clCreateContext(0, 1, &m_clDevice_id, NULL, NULL, &iError);
if (iError != CL_SUCCESS)
{
TRACE("Error creating context \n");
exit(iError);
}
// Command-queue
m_clQueue = clCreateCommandQueue(m_clContext, m_clDevice_id, 0, &iError);
if (iError != CL_SUCCESS)
{
TRACE("Error creating command queue \n");
exit(iError);
}
上面的代码没什么特殊的,照着写就行了
CString strPath;
GetCurrentDirectory(MAX_PATH,strPath.GetBuffer(MAX_PATH));//获得当前路径
strPath.ReleaseBuffer();
CString strNewPath = strPath+ "\\testgpu.cl";//找cl文件
size_t src_size = 0;
CFileFind ff;
if(!ff.FindFile(strNewPath))
{
AfxMessageBox("在当前目录没有找到OpenCL的文件testgpu.cl");
return FALSE;
}
ff.Close();
//读取*.cl中的内容到内存
CFile *pFile =NULL;
try
{
pFile = new CFile(strNewPath,CFile::modeRead);
}
catch (CException* e)
{
e->Delete();
AfxMessageBox(" 打开文件testgpu.cl 出错");
return FALSE;
}
int iFileLen = pFile->GetLength();
const char* source = new char[iFileLen + 1];
ZeroMemory((void*)source,iFileLen + 1);
pFile->Read((void *)source,iFileLen);//读取文件
delete pFile;
pFile = NULL;
// Creates the program
m_clProgram = clCreateProgramWithSource(m_clContext, 1, &source, &src_size, &iError);//加载文件内容
ASSERT(iError == CL_SUCCESS);
delete source;
source = NULL;
// Builds the program
iError = clBuildProgram(m_clProgram, 1, &m_clDevice_id, NULL, NULL, NULL);//编译cl程序
ASSERT(iError == CL_SUCCESS);
// Shows the log
char* build_log;
size_t log_size;
// First call to know the proper size
clGetProgramBuildInfo(m_clProgram, m_clDevice_id, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
build_log = new char[log_size+1];//编译CL的出错记录
// Second call to get the log
clGetProgramBuildInfo(m_clProgram, m_clDevice_id, CL_PROGRAM_BUILD_LOG, log_size, build_log, NULL);
build_log[log_size] = '\0';
CString strLog(build_log);
TRACE(strLog + "\n");//因为cl程序是在运行时编译的,在运行过程中如果出错,显示编译CL文件的错误,以便查找问题
delete build_log;
build_log = NULL;
创建两个Kernel对应两个函数
// Extracting the kernel
m_clKernel = clCreateKernel(m_clProgram, "Filter_GPU_Single", &iError);//单通道滤波,这个引号中的字符串要对应cl文件中的kernel函数
ASSERT(iError == CL_SUCCESS);
m_clKernel1 = clCreateKernel(m_clProgram, "Filter_GPU_Multi", &iError);//多通道滤波,这个引号中的字符串要对应cl文件中的kernel函数
ASSERT(iError == CL_SUCCESS);
m_bInitCL = TRUE;//初始化成功
return TRUE;
}
释放就比较简单
BOOL CFilter::Uninit()
{//释放资源
if(!m_bInitCL)
{
return FALSE;
}
clReleaseKernel(m_clKernel);
clReleaseKernel(m_clKernel1);
clReleaseCommandQueue(m_clQueue);
clReleaseContext(m_clContext);
return TRUE;
}
下面开始写滤波函数
//用OpenCL(GPU)计算单通道滤波 iDataNum 数据总个数
BOOL CFilter::Filter_GPU(float *pBufferIn,float *pBuferOut,const int iDataNum)
{
if(!m_bInitCL || !m_bInitFilter)
{
return FALSE;
}
if(pBufferIn == NULL || pBuferOut == NULL || iDataNum <=0 )
{
return FALSE;
}
cl_int iError = 0; // Used to handle iError codes
const int iMem_size = sizeof(float)*m_iFilterLen;
int iSrcLen = m_iFilterLen + iDataNum -1;
if(m_pSrcBuffer == NULL || m_iSrcBufferLen < iSrcLen)
{//没有申请缓冲或者缓冲太小,需要申请缓冲
if(m_pSrcBuffer)
{//删除重新申请
delete m_pSrcBuffer;
m_pSrcBuffer = NULL;
}
m_iSrcBufferLen = iSrcLen;
m_pSrcBuffer = new float[m_iSrcBufferLen];
}
//准备数据
memcpy(m_pSrcBuffer,m_pDataSave + 1,(m_iFilterLen - 1) * sizeof(float));//把上一次的数据尾的数据拷到源数据缓冲头
memcpy(m_pSrcBuffer + m_iFilterLen -1,pBufferIn,iDataNum * sizeof(float));//拷贝源数据
memcpy(m_pDataSave,pBufferIn + iDataNum - m_iFilterLen,m_iFilterLen * sizeof(float));//把本次的数据尾的数据暂存,以便下次使用
//建立CL缓冲
cl_mem pSrcBuffer_CL = clCreateBuffer(m_clContext, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, iSrcLen * sizeof(float), m_pSrcBuffer, &iError); //源数据
cl_mem FilterBuffer_CL = clCreateBuffer(m_clContext, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, iMem_size, m_pFilterBuffer, &iError); //滤波系数
cl_mem pBufferOut_CL = clCreateBuffer(m_clContext, CL_MEM_WRITE_ONLY, iSrcLen * sizeof(float), NULL, &iError); //结果
const int iFilterLen = m_iFilterLen;
//设置Kernel函数参数
iError = clSetKernelArg(m_clKernel, 0, sizeof(cl_mem), &pSrcBuffer_CL);
iError |= clSetKernelArg(m_clKernel, 1, sizeof(cl_mem), &FilterBuffer_CL);
iError |= clSetKernelArg(m_clKernel, 2, sizeof(cl_mem), &pBufferOut_CL);
iError |= clSetKernelArg(m_clKernel, 3, sizeof(size_t), &iDataNum);
iError |= clSetKernelArg(m_clKernel, 4, sizeof(size_t), &iFilterLen);
// 执行kernel函数
const size_t global_ws = iDataNum; // Total number of work-items
iError = clEnqueueNDRangeKernel(m_clQueue, m_clKernel, 1, NULL, &global_ws, NULL, 0, NULL, NULL);
//读取结果数据
iError = clEnqueueReadBuffer(m_clQueue, pBufferOut_CL, CL_TRUE, 0, iDataNum * sizeof(float), pBuferOut, 0, NULL, NULL);
//释放CL缓冲
clReleaseMemObject(pSrcBuffer_CL);
clReleaseMemObject(FilterBuffer_CL);
clReleaseMemObject(pBufferOut_CL);
return TRUE;
}
//用OpenCL(GPU)多通道滤波,iDataNum 数据总个数,iFrameLen 帧长度bFrameOrder = TRUE 按帧排列,bFrameOrder = FALSE 按通道排列
BOOL CFilter::Filter_GPU(float *pBufferIn,float *pBuferOut,const int iDataNum,const int iFrameLen,BOOL bFrameOrder)
{
if(!m_bInitCL || !m_bInitFilter)
{
return FALSE;
}
if(pBufferIn == NULL || pBuferOut == NULL || iDataNum <=0 || iFrameLen <=0)
{
return FALSE;
}
cl_int iError = 0; // Used to handle iError codes
int i =0;
int iSrcLen = ((m_iFilterLen - 1 ) *iFrameLen + iDataNum );
if(m_pSrcBuffer == NULL || m_iSrcBufferLen < iSrcLen)
{//没有申请缓冲或者缓冲太小,需要申请缓冲
if(m_pSrcBuffer)
{//删除重新申请
delete m_pSrcBuffer;
m_pSrcBuffer = NULL;
}
m_iSrcBufferLen = iSrcLen;
m_pSrcBuffer = new float[m_iSrcBufferLen];
}
//准备数据
int iFrames = iDataNum / iFrameLen;
int iBlockLen = iFrames + m_iFilterLen - 1;
if(bFrameOrder)
{//按帧排列
for(i =0;i< iFrameLen;i++)
{//把上一次的数据尾的数据拷到源数据的每个段缓冲头
memcpy(m_pSrcBuffer + i * iBlockLen,m_pDataSave + i * m_iFilterLen + 1,(m_iFilterLen - 1) * sizeof(float));
}
for(i =0;i< iFrames;i++)
{
for(int j=0;j< iFrameLen;j++)
{
m_pSrcBuffer[j * iBlockLen + m_iFilterLen - 1 + i] = pBufferIn[i * iFrameLen + j];
}
}
for(i =0;i< iFrameLen;i++)
{//把本次的数据尾的数据暂存,以便下次使用
memcpy(m_pDataSave + i * m_iFilterLen ,m_pSrcBuffer + i * iBlockLen + iBlockLen - m_iFilterLen,(m_iFilterLen - 1) * sizeof(float));
}
}
else
{//按通道排列
for(i =0;i< iFrameLen;i++)
{
memcpy(m_pSrcBuffer + i * iBlockLen,m_pDataSave + i * m_iFilterLen + 1,(m_iFilterLen - 1) * sizeof(float));//把上一次的数据尾的数据拷到源数据的每个段的缓冲头
memcpy(m_pSrcBuffer+ i * iBlockLen + m_iFilterLen - 1,pBufferIn + i * iFrames,iFrames * sizeof(float));//拷贝原始数据
memcpy(m_pDataSave + i * m_iFilterLen,m_pSrcBuffer + i * iBlockLen + iBlockLen - m_iFilterLen ,m_iFilterLen * sizeof(float));//把本次的数据尾的数据暂存,以便下次使用
}
}
const int iFilter_size = sizeof(float) * m_iFilterBufferLen;
//创建CL缓冲
cl_mem pSrcBuffer_CL = clCreateBuffer(m_clContext, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, iSrcLen * sizeof(float), m_pSrcBuffer, &iError); //源数据
cl_mem FilterBuffer_CL = clCreateBuffer(m_clContext, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, iFilter_size, m_pFilterBuffer, &iError); //滤波系数
cl_mem pBufferOut_CL = clCreateBuffer(m_clContext, CL_MEM_WRITE_ONLY, iSrcLen * sizeof(float), NULL, &iError); //结果
const int iFilter = m_iFilterLen;
iError = clSetKernelArg(m_clKernel1, 0, sizeof(cl_mem), &pSrcBuffer_CL);
iError |= clSetKernelArg(m_clKernel1, 1, sizeof(cl_mem), &FilterBuffer_CL);
iError |= clSetKernelArg(m_clKernel1, 2, sizeof(cl_mem), &pBufferOut_CL);
iError |= clSetKernelArg(m_clKernel1, 3, sizeof(size_t), &iDataNum);
iError |= clSetKernelArg(m_clKernel1, 4, sizeof(size_t), &iFilter);
iError |= clSetKernelArg(m_clKernel1, 5, sizeof(size_t), &iFrameLen);
// 执行kernel
const size_t global_ws = iDataNum; // Total number of work-items
iError = clEnqueueNDRangeKernel(m_clQueue, m_clKernel1, 1, NULL, &global_ws, NULL, 0, NULL, NULL);
//读取结果
iError = clEnqueueReadBuffer(m_clQueue, pBufferOut_CL, CL_TRUE, 0, iDataNum * sizeof(float), pBuferOut, 0, NULL, NULL);
//结果是按通道存储的,因此,如果要按帧输出,需要转换
//释放CL缓冲
clReleaseMemObject(pSrcBuffer_CL);
clReleaseMemObject(FilterBuffer_CL);
clReleaseMemObject(pBufferOut_CL);
return TRUE;
}
Kernel 函数 ,写在testgpu.cl文件中
__kernel void Filter_GPU_Multi(__global const float* pSrcBuffer_CL,__global const float* pFilterBuffer_CL,__global float* pBufferOut_CL,const int iNum,const int iFilterNum,const int iFrameLen)
{//多通道滤波
const int idx = get_global_id(0);
if (idx < iNum)
{
int iBlockLen = iNum / iFrameLen;
int iFrames = idx / iBlockLen;
float fSum =0.0f;
for(int i = 0;i< iFilterNum;i++)
{
fSum += pSrcBuffer_CL[iFrames * (iFilterNum -1 + iBlockLen ) + idx % iBlockLen + i] * pFilterBuffer_CL[iFrames * iFilterNum + i];
}
pBufferOut_CL[idx] = fSum;
}
}
__kernel void Filter_GPU_Single(__global const float* pSrcBuffer_CL,__global const float* pFilterBuffer_CL,__global float* pBufferOut_CL,const int iNum,const int iFilterNum)
{//单通道滤波
const int idx = get_global_id(0);
if (idx < iNum)
{
float fSum =0.0f;
for(int i = 0;i< iFilterNum;i++)
{
fSum += pSrcBuffer_CL[idx + i] * pFilterBuffer_CL[i];
}
pBufferOut_CL[idx] = fSum;
}
}
为了验证用GPU计算的结果是否正确,还需要写CPU计算的程序,以便验证其正确性。CPU计算此处就不贴代码了,完整代码请到OpenCL实现单通道和多通道数字滤波-C++代码类资源-CSDN下载 下载。
以上程序在win7 CUDA SDK 4.2 和 VC++ 2008 + SP1 编译通过。能够正常运行。运行结果GPU和CPU运算结果是一致的。由于每次计算后,m_pDataSave的内容发生变化,输出缓冲前面一段数据可能会不一样。如果要测试GPU和CPU两种方式的结果是否一样,需要每次在滤波前把m_pDataSave的内容设成一样。
如果VS2008没有SP1,需要修改stdafx.h文件
删除下面这行
#include <afxcontrolbars.h> // 功能区和控件条的MFC 支持
添加
#ifdef CWinAppEx
#undef CWinAppEx
#endif
#define CWinAppEx CWinApp
结尾:
由于第一次写OpenCL程序,错误在所难免。而且对于cl的滤波函数,也没有很好的算法,也没花时间去研究。只能算是实验OpenCL的一个测试程序。对于GPU加速的程序,需要好的算法。对于程序员来说,需要改变以前的思维习惯,以前都用单一的计算单元在做计算,即便用到多线程,也是基于任务的,也就是说A线程做一件事,B线程做另外一件事。对于运算,一般在循环中顺序执行。而GPU由于内核多,是并发执行的,因此需要考虑其并发性和乱序执行。如果数据量小的话,其数据IO所占的时间往往比执行所占的时间多,这时使用GPU还不如使用CPU快。因此,好的算法以及运算量大的程序,才能感觉到GPU加速的优势。要写好OpenCL程序,以后的路还很长。(什么时候我们的程序由CPU和GPU自动调节运行就好了,对用户和程序员均不透明,就像双核或者多核CPU一样,我们根本就不用关心程序或者说某个线程在哪个核上运行!呵呵)。