前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >opencl:cl::make_kernel的进化

opencl:cl::make_kernel的进化

作者头像
10km
发布2019-05-25 22:42:35
1.3K0
发布2019-05-25 22:42:35
举报
文章被收录于专栏:10km的专栏10km的专栏10km的专栏

版权声明:本文为博主原创文章,转载请注明源地址。 https://cloud.tencent.com/developer/article/1433791

我之前的一篇博客《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调用都要有上面两个动作,概括起来就是

  1. 在执行kernel之前,如果kernel参数中有指针类型或imag类型的参数,需要将参数在主机端对应的cl::Memory类型(其子类包括cl::Image,cl::Buffer)的数据上传(upload)到设备
  2. 在执行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_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进化

但是好像当我准备将这个run_kernel,用于执行第二个kernel函数时,问题来了。

我们看上面这个run_kernel函数,它对kernel函数的参数类型和顺序是有要求的:

  1. 第一个参数必须是输入的数据对象
  2. 第二个参数必须是输出数据对象
  3. 其他标量数据对象必须位于第三位以后

所以,它的使用是有限制的,我的第二个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_needdownload_args是个模板函数啊,

嗯,在这里用了递归模板函数,循环检查args 参数表中的参数类型,如果是memory_cl类就执行memory_cl中的upload_if_need函数,

download_args也是差不多,如果是memory_cl类就根据download标记执行memory_cl中的download函数

upload_args_if_needdownload_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函数要执行时,使用这种设计方式可以大大减少撰写重复或相似的代码,同时增加代码的稳定性。

神奇的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<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;
};
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年03月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • make_kernel进化之run_kernel
  • run_kernel进化
    • 进化之一
      • 进化之二
        • 进化之三
          • 总结
          • 神奇的memory_cl
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档