opencl:cl::make_kernel的进化
我之前的一篇博客《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::CommandQueuethis->upload(command_queue);//向OpenCL设备中上传原始图像数据cl_float widthNormalizationFactor = 1.0f / dst_width;cl_float heightNormalizationFactor = 1.0f / dst_height;//构造cl::make_kernel对象执行kernelcl::make_kernel(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调用都要有上面两个动作,概括起来就是
- 在执行kernel之前,如果kernel参数中有指针类型或imag类型的参数,需要将参数在主机端对应的cl::Memory类型(其子类包括cl::Image,cl::Buffer)的数据上传(upload)到设备
- 在执行kernel结束后,可能需要将kernel处理之后的输出数据(同样是cl::Memory类型)下载(download)到主机。
这些都是重复和类似的代码,我们只要把这两个动作抽象出来(memory_cl类),就可以有办法将这两个动作也封装起来。
关于如何实现memory_cl类,将要本文后面讲到,现在假定我们已经有memory_cl类实现对所有cl::Memory对象的download和upload统一管理
make_kernel进化之run_kernel
于是利用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 // 输入数据对象,memory_cl为自已写的opencl内存管理类,memory_cl&out// 输出数据对象,memory_cl为自已写的opencl内存管理类,Args&&... args //其他kernel参数){// 根据数据状态标记判断是否需要上传数据到设备,如果数据已经在设备中就不需要uploadin.upload_if_need(queue_args.queue_);// 执行kernelcl::make_kernelk(kernel);//创建cl::make_kernel对象k(queue_args,in.cl_mem_obj,out.cl_mem_obj, std::forward(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<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进化
但是好像当我准备将这个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)...);typename make_make_kernel::type k(kernel);k(queue_args,std::forward(args)...); // 执行kernel// 根据download标记需要下载所有cl::Memory输出对象的数据到主机download_args<1>(queue_args.queue_,download,std::forward(args)...);
}
额,粗看起来与前一版本的run_kernel,貌似差不多,
但还是它真的是进化了
进化之一
只是参数中不再有in,out参数,也就是说,参数表中可以不用关心in/out参数的顺序以及个数了。
,const memory_cl &in,memory_cl&out
进化之二
与前一版本的run_kernel相比,原来第一行的 (queue_args.queue_,std::forward (queue_args.queue_,download,std::forwardin.upload_if_need(queue_args.queue_);换成了upload_args_if_need最后一行的out.download(queue_args.queue_);换成了download_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);static void check(...);using cl_type=decltype(check(std::declval()));enum{value=!std::is_samevoid>::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::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::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();auto queue_context=command_queue.getInfo();// 检查memory_cl中内存对象的context与command_queue是否一致,不一致则抛出异常if(mem_context()!=queue_context()){std::stringstream stream;stream<<":the arg No:"<// 动态参数编号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:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,e,stream.str());}catch(face_exception&e){std::stringstream stream;stream<<"the arg No:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,stream.str());}catch(std::exception&e){std::stringstream stream;stream<<"the arg No:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,e,stream.str());}catch(...){std::stringstream stream;stream<<"the arg No:"<":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 (command_queue,std::forward(arg1));//处理第一个参数upload_args_if_need1> (command_queue,std::forward(args)...);//递归处理其他参数
}
// 参数ARG为非memory_cl类型时,为空函数,啥也不做直接返回
template<int N,typename ARG>
typename std::enable_if::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::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判断是否为输出数据对象,以决定是否需要下载数据if(flags&(CL_MEM_WRITE_ONLY|CL_MEM_READ_WRITE)){const_cast(arg).download(command_queue);//下载数据到设备}}catch(cl::Error&e){std::stringstream stream;stream<<"the arg No:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,e,stream.str());}catch(face_exception&e){std::stringstream stream;stream<<"the arg No:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,stream.str());}catch(std::exception&e){std::stringstream stream;stream<<"the arg No:"<// 动态参数编号throw face_cl_exception(SOURCE_AT,e,stream.str());}catch(...){std::stringstream stream;stream<<"the arg No:"<":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(command_queue,download,std::forward(arg1));//处理第一个参数download_args1>(command_queue,download,std::forward(args)...);//递归处理其他参数
}
进化之三
原来是直接实例化cl::make_kernel类对象的
cl::make_kernel, OUT_CL_TYPE, Args...>k(kernel);
而新版本则改成了
typename make_make_kernel::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::type,typename MEM_CL= is_kind_of_memory_cl,typename K_TYPE=typename std::conditionaltypename 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::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type,typename kernel_type::type, typename kernel_type::type>;
};
总结
进化后的run_kernel使用起来了方便多了,对kernel参数个数和顺序不再有限制,同时自动实现OpenCL内存对象数据的上传和下载。
只是代码貌似增加了好多好多,实现增加的代码主要是模板函数,都只是在编译期起作用,并不会增加多少运行时代码。
它带来的好处是当你的项目中有很多不同的kernel函数要执行时,使用这种设计方式可以大大减少撰写重复或相似的代码,同时增加代码的稳定性。
神奇的memory_cl
前面一直不断被提起的用来封装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::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 &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 &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::value>::typeupload_force(const std::vector &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::value>::typeupload_force(const std::vector &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::value>::typedownload_force(std::vector &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::value>::typedownload_force(std::vector &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;
};
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
