我之前的一篇博客《opencl:C++ 利用cl::make_kernel简化kernel执行代码》详细说明了如何使用OpenCL C++接口(cl.hpp)提供cl::make_kernel算子来简化kernel执行代码。
/* 缩放图像(双线性插值) */
gray_matrix_cl gray_matrix_cl::zoom(size_t dst_width, size_t dst_height, const facecl_context& context)const {
gray_matrix_cl dst_matrix(dst_width, dst_height);
auto command_queue = global_facecl_context.getCommandQueue();// 获取cl::CommandQueue
this->upload(command_queue);//向OpenCL设备中上传原始图像数据
cl_float widthNormalizationFactor = 1.0f / dst_width;
cl_float heightNormalizationFactor = 1.0f / dst_height;
//构造cl::make_kernel对象执行kernel
cl::make_kernel<cl::Image2D,cl::Image2D,cl_float,cl_float>
(context.getKernel(KERNEL_NAME(image_scaling)))// 获取已经编译好的cl::Kernel
(cl::EnqueueArgs(command_queue,cl::NDRange( dst_width, dst_height )),
cl_img,dst_matrix.cl_img,
widthNormalizationFactor,
heightNormalizationFactor);
command_queue.finish(); // 等待kernel执行结束
dst_matrix.download(command_queue);从OpenCL设备中下载结果数据
return std::move(dst_matrix);
}
这是上一篇博客中最后简化的代码。与原来原始代码相比,这种调用方式将所有设置kernel参数的调用(setArg
)都被cl::make_kernel
算子(fuctor)封装,调用者不需要知道细节。只需要执行cl::make_kernel
的operator(),在()中按kernel定义的参数顺序将kernel需要的参数填在括号中,cl::make_kernel
算子会自动为kernel设置参数并将kernel压入command_queue执行。
Ok,前一篇博客的内容回顾完毕。
那么还能不能进一步改进,让kernel执行更简单化?
再看看上面的代码,在用opencl的kernel执行一个图像的缩放之前,先要
this->upload(command_queue);//向OpenCL设备中上传原始图像数据
在kernel执行结束之后,
dst_matrix.download(command_queue);从OpenCL设备中下载结果数据
在你写完第一个kernel程序后,再写另外一个kernel的时候,你会发现几乎所有的kernel调用都要有上面两个动作,概括起来就是
这些都是重复和类似的代码,我们只要把这两个动作抽象出来(memory_cl类),就可以有办法将这两个动作也封装起来。
关于如何实现memory_cl类,将要本文后面讲到,现在假定我们已经有memory_cl类实现对所有cl::Memory对象的download和upload统一管理
于是利用C++11的变长模板特性,我们可以写出下面的run_kernel
模板函数
template<typename IN_CL_TYPE // kernel参数中的输入数据类型(cl::Buffer,cl::Image)
,typename OUT_CL_TYPE// kernel参数中的输出数据类型(cl::Buffer,cl::Image)
,typename... Args // kernel参数中其他标量数据类型,变长模板,允许多个参数
>
void run_kernel(const cl::EnqueueArgs &queue_args // 队列参数
,const cl::Kernel &kernel//kernel对象
,bool download //kernel执行结束后是否将结果数据下载到本地?
,const memory_cl<IN_CL_TYPE> &in // 输入数据对象,memory_cl为自已写的opencl内存管理类
,memory_cl<OUT_CL_TYPE>&out// 输出数据对象,memory_cl为自已写的opencl内存管理类
,Args&&... args //其他kernel参数
){
// 根据数据状态标记判断是否需要上传数据到设备,如果数据已经在设备中就不需要upload
in.upload_if_need(queue_args.queue_);
// 执行kernel
cl::make_kernel<IN_CL_TYPE, OUT_CL_TYPE, Args...>k(kernel);//创建cl::make_kernel对象
k(queue_args,in.cl_mem_obj,out.cl_mem_obj, std::forward<Args>(args)...);//执行kernel
// 根据download标记决定是否执行 memory_cl的download函数将kernel输出数据下载到主机。
if(download)
out.download(queue_args.queue_);
}
借助这个run_kernel
模板函数,前面实现图像缩放的gray_matrix_cl::zoom
函数就可以改写如下:
template<typename T>T get_align(T v,uint8_t a){return (T)((v+(T)((1<<a)-1))>>a);}
/* 缩放图像(双线性插值) */
gray_matrix_cl gray_matrix_cl::zoom(size_t dst_width, size_t dst_height, const facecl_context& context,bool download)const {
gray_matrix_cl dst_matrix(dst_width, dst_height);
auto command_queue = global_facecl_context.getCommandQueue();
cl_float widthNormalizationFactor = 1.0f / dst_width;
cl_float heightNormalizationFactor = 1.0f / dst_height;
run_kernel(
cl::EnqueueArgs(command_queue,{ 1, get_align(dst_height,4) })//队列参数对象
,context.getKernel(KERNEL_NAME(image_scaling)) // 要执行的kernel对象
,true //自动下载结果数据
,*this //输入图像
,dst_matrix // 输出图像
,widthNormalizationFactor
,heightNormalizationFactor
);
command_queue.finish(); // 等待kernel执行结束
return std::move(dst_matrix);
}
哈哈,这样以来代码又简化了,大功告成!
但是好像当我准备将这个run_kernel
,用于执行第二个kernel函数时,问题来了。
我们看上面这个run_kernel
函数,它对kernel函数的参数类型和顺序是有要求的:
所以,它的使用是有限制的,我的第二个kernel函数,只有一个数据对象参数,它即是输入又是输出,它就不太方便用这个函数,(当然还是可以用,将这参数重复填入两次)
当kernel函数有超一个输入数据对象或输出数据对象,就没可能用这个模板函数。。。
能不能改进run_kernel
函数,使它允许接收超过一个输入/出数据对象参数,并且不用限定kernel的参数顺序呢?
yes,we can
run_kernel要经历再一次的进化!
下面是改进后的run_kernel
模板函数
template<typename... Args>
inline void run_kernel_new(const cl::EnqueueArgs &queue_args// 队列参数对象
, const cl::Kernel &kernel // kernel对象
, bool download // kernel执行结束后是否下载结果数据
, Args&&... args // kernel参数表
){
// 根据需要上传所有cl::Memory对象的数据到设备
upload_args_if_need<1>(queue_args.queue_,std::forward<Args>(args)...);
typename make_make_kernel<Args...>::type k(kernel);
k(queue_args,std::forward<Args>(args)...); // 执行kernel
// 根据download标记需要下载所有cl::Memory输出对象的数据到主机
download_args<1>(queue_args.queue_,download,std::forward<Args>(args)...);
}
额,粗看起来与前一版本的run_kernel
,貌似差不多,
但还是它真的是进化了
只是参数中不再有in,out参数,也就是说,参数表中可以不用关心in/out参数的顺序以及个数了。
,const memory_cl<IN_CL_TYPE> &in
,memory_cl<OUT_CL_TYPE>&out
与前一版本的run_kernel相比,原来第一行的in.upload_if_need(queue_args.queue_);
换成了upload_args_if_need<1>(queue_args.queue_,std::forward<Args>(args)...);
最后一行的out.download(queue_args.queue_);
换成了download_args<1>(queue_args.queue_,download,std::forward<Args>(args)...);
等等, 这upload_args_if_need
和download_args
是个模板函数啊,
嗯,在这里用了递归模板函数,循环检查args 参数表中的参数类型,如果是memory_cl
类就执行memory_cl
中的upload_if_need
函数,
download_args
也是差不多,如果是memory_cl
类就根据download
标记执行memory_cl
中的download
函数
upload_args_if_need
和download_args
模板函数的实现如下:
/* 模板函数,检查T是否为memory_cl的子类 */
template<typename T>
struct is_kind_of_memory_cl{
template <typename CL_TYPE>
static CL_TYPE check(memory_cl<CL_TYPE>);
static void check(...);
using cl_type=decltype(check(std::declval<T>()));
enum{value=!std::is_same<cl_type,void>::value};
};
/*
* upload_arg(x)_if_need和download_arg(x)系列模板函数循环对run_kernel中的所有变长参数类型进行识别,
* 对于memory_cl类型的参数,根据需要在kernel执行前上传数据到设备,
* 并在kernel执行后根据需要下载输出数据到主机
* 模板中的N参数,用于调试时知道哪个参数出错
*
* */
// 参数ARG为非memory_cl类型时直接返回,啥也不做
template<int N,typename ARG>
typename std::enable_if<!is_kind_of_memory_cl<ARG>::value>::type
inline upload_arg_if_need(const cl::CommandQueue &command_queue,const ARG & arg){}
// 参数ARG是memory_cl类型,时根据需要上传数据
template<int N,typename ARG>
typename std::enable_if<is_kind_of_memory_cl<ARG>::value>::type
inline upload_arg_if_need(const cl::CommandQueue &command_queue,const ARG & arg){
const cl::Memory&m=arg.cl_mem_obj;
auto mem_context=m.getInfo<CL_MEM_CONTEXT>();
auto queue_context=command_queue.getInfo<CL_QUEUE_CONTEXT>();
// 检查memory_cl中内存对象的context与command_queue是否一致,不一致则抛出异常
if(mem_context()!=queue_context()){
std::stringstream stream;
stream<<":the arg No:"<<N;// 动态参数编号
throw std::invalid_argument(std::string(SOURCE_AT).append(stream.str()).append(":mem_context()!=queue_context()"));
}
try{
arg.upload_if_need(command_queue);//上传数据到设备
}catch(cl::Error&e){
std::stringstream stream;
stream<<"the arg No:"<<N;// 动态参数编号
throw face_cl_exception(SOURCE_AT,e,stream.str());
}catch(face_exception&e){
std::stringstream stream;
stream<<"the arg No:"<<N<<e.what();// 动态参数编号
throw face_cl_exception(SOURCE_AT,stream.str());
}catch(std::exception&e){
std::stringstream stream;
stream<<"the arg No:"<<N;// 动态参数编号
throw face_cl_exception(SOURCE_AT,e,stream.str());
}catch(...){
std::stringstream stream;
stream<<"the arg No:"<<N<<":unknow exception";// 动态参数编号
throw face_cl_exception(SOURCE_AT,stream.str());
}
}
// 特例:参数表为空,递归终止
template<int N>
inline void upload_args_if_need(const cl::CommandQueue &command_queue){
}
/* 递归处理Args中的每一个参数
* 如果是memory_cl类型的对象,则上传数据到设备
* */
template<int N,typename ARG1,typename... Args>
inline void upload_args_if_need(const cl::CommandQueue &command_queue,ARG1 && arg1,Args&&... args){
upload_arg_if_need<N> (command_queue,std::forward<ARG1>(arg1));//处理第一个参数
upload_args_if_need<N+1> (command_queue,std::forward<Args>(args)...);//递归处理其他参数
}
// 参数ARG为非memory_cl类型时,为空函数,啥也不做直接返回
template<int N,typename ARG>
typename std::enable_if<!is_kind_of_memory_cl<ARG>::value>::type
inline download_arg(const cl::CommandQueue &command_queue,bool download, const ARG & arg){}
// 参数ARG是memory_cl类型,时根据需要下载数据到主机
template<int N,typename ARG>
typename std::enable_if<is_kind_of_memory_cl<ARG>::value>::type
inline download_arg(const cl::CommandQueue &command_queue,bool download, const ARG & arg){
if(download){
try{
const cl::Memory &m=arg.cl_mem_obj;
auto flags=m.getInfo<CL_MEM_FLAGS>();
// 根据CL_MEM_FLAGS判断是否为输出数据对象,以决定是否需要下载数据
if(flags&(CL_MEM_WRITE_ONLY|CL_MEM_READ_WRITE)){
const_cast<ARG&>(arg).download(command_queue);//下载数据到设备
}
}catch(cl::Error&e){
std::stringstream stream;
stream<<"the arg No:"<<N;// 动态参数编号
throw face_cl_exception(SOURCE_AT,e,stream.str());
}catch(face_exception&e){
std::stringstream stream;
stream<<"the arg No:"<<N<<e.what();// 动态参数编号
throw face_cl_exception(SOURCE_AT,stream.str());
}catch(std::exception&e){
std::stringstream stream;
stream<<"the arg No:"<<N;// 动态参数编号
throw face_cl_exception(SOURCE_AT,e,stream.str());
}catch(...){
std::stringstream stream;
stream<<"the arg No:"<<N<<":unknow exception";// 动态参数编号
throw face_cl_exception(SOURCE_AT,stream.str());
}
}
}
// 特例:参数表为空,递归终止
template<int N>
inline void download_args(const cl::CommandQueue &command_queue,bool download){}
/* 递归处理Args中的每一个参数
* 如果是memory_cl类型的对象,则根据download参数的指示下载数据到主机
* */
template<int N,typename ARG1,typename... Args>
inline void download_args(const cl::CommandQueue &command_queue,bool download, ARG1 && arg1,Args&&... args){
download_arg<N>(command_queue,download,std::forward<ARG1>(arg1));//处理第一个参数
download_args<N+1>(command_queue,download,std::forward<Args>(args)...);//递归处理其他参数
}
原来是直接实例化cl::make_kernel类对象的
cl::make_kernel<IN_CL_TYPE, OUT_CL_TYPE, Args...>k(kernel);
而新版本则改成了
typename make_make_kernel<Args...>::type k(kernel);
这里make_make_kernel
也是一个模板函数,用来实例化cl::make_kernel
类,为什么要这么做呢?
因为传递给run_kernel
的参数中所有OpenCL内存对象(cl::Buffer,cl::Image
)都被我自定义的memeory_cl
类封装起来了,而cl::make_kernel
在执行的时候,参数类型却是需要原始的OpenCL内存对象(cl::Buffer,cl::Image
),所以实例化cl::make_kernel
时必须将memeory_cl
类型转为对应的OpenCL内存对象类型。
make_make_kernel
模板函数就是实现这个功能的,下面是make_make_kernel
的代码实现
/* 模板函数返回make_kernel执行里需要的类
* 对于普通的类,就是类本身
* 对于memory_cl的子类,返回memory_cl::cl_cpp_type
* */
template<typename ARG
,typename ARG_TYPE=typename std::decay<ARG>::type
,typename MEM_CL= is_kind_of_memory_cl<ARG>
,typename K_TYPE=typename std::conditional<MEM_CL::value,typename MEM_CL::cl_type,ARG>::type
>
struct kernel_type {
using type= K_TYPE;
};
/*
* 模板函数
* 根据模板参数,创建cl::make_kernel类
* 创建cl::make_kernel类时所有的模板参数都会调用 kernel_type模板函数,
* 以获取实例化cl::make_kernel时真正需要的类型
*/
template <
typename T0, typename T1 = cl::detail::NullType, typename T2 = cl::detail::NullType,
typename T3 = cl::detail::NullType, typename T4 = cl::detail::NullType,
typename T5 = cl::detail::NullType, typename T6 = cl::detail::NullType,
typename T7 = cl::detail::NullType, typename T8 = cl::detail::NullType,
typename T9 = cl::detail::NullType, typename T10 = cl::detail::NullType,
typename T11 = cl::detail::NullType, typename T12 = cl::detail::NullType,
typename T13 = cl::detail::NullType, typename T14 = cl::detail::NullType,
typename T15 = cl::detail::NullType, typename T16 = cl::detail::NullType,
typename T17 = cl::detail::NullType, typename T18 = cl::detail::NullType,
typename T19 = cl::detail::NullType, typename T20 = cl::detail::NullType,
typename T21 = cl::detail::NullType, typename T22 = cl::detail::NullType,
typename T23 = cl::detail::NullType, typename T24 = cl::detail::NullType,
typename T25 = cl::detail::NullType, typename T26 = cl::detail::NullType,
typename T27 = cl::detail::NullType, typename T28 = cl::detail::NullType,
typename T29 = cl::detail::NullType, typename T30 = cl::detail::NullType,
typename T31 = cl::detail::NullType
>
struct make_make_kernel{
using type=cl::make_kernel<
typename kernel_type<T0>::type, typename kernel_type<T1>::type,
typename kernel_type<T2>::type, typename kernel_type<T3>::type,
typename kernel_type<T4>::type, typename kernel_type<T5>::type,
typename kernel_type<T6>::type, typename kernel_type<T7>::type,
typename kernel_type<T8>::type, typename kernel_type<T9>::type,
typename kernel_type<T10>::type, typename kernel_type<T11>::type,
typename kernel_type<T12>::type, typename kernel_type<T13>::type,
typename kernel_type<T14>::type, typename kernel_type<T15>::type,
typename kernel_type<T16>::type, typename kernel_type<T17>::type,
typename kernel_type<T18>::type, typename kernel_type<T19>::type,
typename kernel_type<T20>::type, typename kernel_type<T21>::type,
typename kernel_type<T22>::type, typename kernel_type<T23>::type,
typename kernel_type<T24>::type, typename kernel_type<T25>::type,
typename kernel_type<T26>::type, typename kernel_type<T27>::type,
typename kernel_type<T28>::type, typename kernel_type<T29>::type,
typename kernel_type<T30>::type, typename kernel_type<T31>::type
>;
};
进化后的run_kernel
使用起来了方便多了,对kernel参数个数和顺序不再有限制,同时自动实现OpenCL内存对象数据的上传和下载。
只是代码貌似增加了好多好多,实现增加的代码主要是模板函数,都只是在编译期起作用,并不会增加多少运行时代码。
它带来的好处是当你的项目中有很多不同的kernel函数要执行时,使用这种设计方式可以大大减少撰写重复或相似的代码,同时增加代码的稳定性。
前面一直不断被提起的用来封装OpenCL内存对象的memory_cl
是个什么神奇的东东?呵呵,其实并不复杂,就是抽象的基类而已,下面是这个类的主要实现代码和函数声明。前面代码所涉及到的所有函数都在这里有声明。
/*
* OpenCL内存抽象模型定义
* memory_cl为抽象接口,所有OpenCL内存对象(cl::Buffer,cl::Image等等)都被封装在该对象内部
* 主要提供主机与设备之间的交换功能
* 项目中涉及的其他涉及OpenCL内存对象的类都是此类的衍生类
* matrix_cl 继承自memory_cl,是抽象矩阵类
* integral_matrix继承自matrix_cl,积分图对象类
* gray_matrix_cl继承自matrix_cl,灰度图像类
* */
template<typename CL_TYPE,
typename ENABLE=typename std::enable_if<std::is_base_of<cl::Memory,CL_TYPE>::value>::type>
class memory_cl{
public:
using cl_cpp_type=CL_TYPE;
private:
mutable bool on_device=false; // 数据是否已经在设备上标志
public:
cl_cpp_type cl_mem_obj; // OpenCL 内存对象
/* 如果数据没有上传到设备(on_device=false),则向OpenCL设备中上传原始矩阵数据,
* 上传成功则将on_device置为true
* */
void upload_if_need(const cl::CommandQueue& command_queue=Null_Queue)const{
if(!on_device){
upload(command_queue);
}
}
/* 虚函数,从OpenCL设备中下载结果数据, 将on_device标志置为true */
virtual void download(const cl::CommandQueue& command_queue=Null_Queue){
throw face_exception(SOURCE_AT,"sub class must implement the funtion "
"by calling download_force(const cl::CommandQueue& command_queue,std::vector<E> &out)");
}
/* 虚函数,向OpenCL设备中上传原始矩阵数据, 将on_device标志置为true */
virtual void upload(const cl::CommandQueue& command_queue=Null_Queue)const{
throw face_exception(SOURCE_AT,
"sub class must implement the funtion "
"by calling upload_force(const cl::CommandQueue& command_queue,std::vector<E> &in) ");
}
/* upload_force上传cl::Memory对象到设备,上传成功则将on_device置为true
* 因为项目中只涉及到使用cl::Buffer和cl::Image2D所以,在此做只分别对cl::Buffer和cl::Image写了相关的代码,
* download_force也是一样
*/
template<typename E, typename _CL_TYPE = CL_TYPE>
typename std::enable_if<std::is_base_of<cl::Buffer,_CL_TYPE>::value>::type
upload_force(const std::vector<E> &in,const cl::CommandQueue& command_queue=Null_Queue) const;
template<typename E,typename _CL_TYPE=CL_TYPE>
typename std::enable_if<std::is_base_of<cl::Image2D,_CL_TYPE>::value>::type
upload_force(const std::vector<E> &in,const cl::CommandQueue& command_queue=Null_Queue) const;
/* 从cl_mem_obj对象中下载数据到out,下载成功则将on_device置为true */
template<typename E, typename _CL_TYPE = CL_TYPE>
typename std::enable_if<std::is_base_of<cl::Buffer,_CL_TYPE>::value>::type
download_force(std::vector<E> &out, const cl::CommandQueue& command_queue=Null_Queue) const;
template<typename E, typename _CL_TYPE = CL_TYPE>
typename std::enable_if<std::is_base_of<cl::Image2D,_CL_TYPE>::value>::type
download_force(std::vector<E> &out,size_t row_pitch=0,const cl::CommandQueue& command_queue=Null_Queue) const;
//相关的构造函数/
memory_cl(const CL_TYPE& cl_mem_obj,bool on_device):cl_mem_obj(cl_mem_obj),on_device(on_device){};
memory_cl(const memory_cl&)=default;
memory_cl(memory_cl&&)=default;
memory_cl()=default;
memory_cl& operator=(const memory_cl&)=default;
memory_cl& operator=(memory_cl&&rv){
this->cl_mem_obj=std::move(rv.cl_mem_obj);
this->on_device=rv.on_device;
return *this;
};
/* operator type()操作符,返回OpenCL内存对象 */
operator const cl_cpp_type& ()const{ return this->cl_mem_obj; }
operator cl_cpp_type&(){return this->cl_mem_obj;}
virtual ~memory_cl()=default;
};