深度学习C++模板推导再炫技:统一CUDA elementwise kernel的调用接口

本文原发自公众号"AI不止算法",欢迎关注,不定时分享技术感悟和就业指导,原文链接在此

背景

elementwise的概念似乎并没有一个官方说法,为此我特意去wiki上查了一下确实没有,如下图。所以好像属于行内用语,一些公开的博客上称elementwise指两个相同shape的张量(tensor),在对应元素上进行逐位运算。但这是不准确的,后半段是正确的,前半段不对,elementwise不仅指两个相同shape的tensor,而且还指一个或三个甚至四个,只是在AI里面三个和四个用得相对少,但是一个是很多的,并且输出也可以是一个tensor或多个tensor。
在这里插入图片描述

举个例子:

一个tensor的elementwise操作包括(又叫unary operation):++,–,!

两个tensor的elementwise操作包括(又叫binary operation): +, -, *, /

问题

由上述可知,所有elementwise kernel的计算特点都是一样的(在对应元素上逐个apply上述operation),不一样的地方仅仅存在两处:

  1. 这个operation是binary中的某某还是unary中的某某还是tenary(三个tensor)中的某某

  2. 这个operation是一个input/output tensor还是两个还是三个

那我们能否将所有的这些elementwise kernel用一个wrapper包装起来,在这个wrapper里面处理不同个数的input tensor,再把它们分发到不同的operation进行计算并写到不同个数的output tensor?

wrapper的实现

以下就是我们wrapper的签名,接收inputs和outputs vector,以及具体的operation function(后文简称op),并且把Operation作为模板参数,交给编译器做模板推导,推导出具体的函数类型,以此泛化接收不同格式的function(函数指针,operator()等等)

template <typename Operation, int NumOuts = 1>
void ElementwiseKernelWrapper(const Device &dev,
                       const std::vector<const Tensor*> &ins,
                       std::vector<Tensor*> *outs,
                       Operation func)

因为每个op的输入个数都可能不一样,所以我们还需要萃取出它们的个数,由以下L9可以拿到输入个数(涉及到萃取的东西,以后文章再细谈)

template <typename>
struct FunctionTraits;

template <typename T>
struct FunctionTraits : public FunctionTraits<decltype(&T::operator())> {};

......

template <typename ReturnType, typename... Args>
struct FunctionTraits<ReturnType(Args...)> {
  static const size_t arity = sizeof...(Args);
  using ArgsTuple = std::tuple<Args...>;
};

有了这样一个萃取的工具后,我们再回到Wrapper,如下,L7拿到了输入tensor的个数,在L9,我们就可以直接launch elementwise kernel。

template <typename Operation, int NumOuts = 1>
void ElementwiseKernelWrapper(const Device &dev,
                       const std::vector<const Tensor*> &ins,
                       std::vector<Tensor*> *outs,
                       Operation func){
  using Traits = FunctionTraits<Operation>;
  const int Arity = Traits::arity;
  // ...some macro checks...
  LaunchElementwiseCudaKernel(...);
 }

那我们再来看看LaunchElementwiseCudaKernel的签名和实现,这里的签名与之前多数一样,重点在于实现,实现的难度在于如何handle不同个数的输入和输出,为了接收不同个数的输入和输出,我把输入个数获取提前到了编译期来获取,也就是上面的函数萃取来获取,至于输出个数,可以看出,作为模板参数直接传进来,而后,使用std::array来接收这些输入buffer和输出buffer,见L6和L7,至此,我们获得了可以handle任意个数输入和输出的能力

template <typename Operation, int Arity, int NumOuts, int VecSize>
void LaunchElementwiseCudaKernel(const Device &dev,
                                 const std::vector<const Tensor*> &ins,
                                 std::vector<Tensor*> *outs,
                                 Operation func)
  std::array<Tin*, Arity> ins_data;
  std::array<Tout*, NumOuts> outs_data;

接下来,我们还需要把ins buffer和outs buffer分别拷贝到ins_data和outs_data里面的每个buffer,类似于是以下这么个操作,ps:其实这里也可以用模板推导来完成,不过这个留在下篇文章来讲吧

  for (int i = 0; i < NumOuts; ++i) {
    outs_data[i] = (TOut *)((*outs)[i]));
  }

接下来,求出grid size和block size之后,即可launch真正的cuda kernel

  ElementwiseKernel<Operation, Arity, NumOuts, VecSize>
      <<<grid_size, block_size, 0, stream>>>(
          ins_data, outs_data, func);

那么这个真正的CUDA elementwise kernel又是怎么写的呢,以及模板推导来如何完成ins buffer和outs buffer分别拷贝到ins_data和outs_data里面的每个buffer的,那么假期后的下篇文章再来揭晓,感谢阅读和三连!

本文原发自公众号"AI不止算法",欢迎关注,不定时分享技术感悟和就业指导,原文链接在此

彩蛋

最后,彩蛋时间

1.最近随着春招和实习的招聘,越来越多来联系我的同学已经拿到了不错的offer,恭喜~

在这里插入图片描述
在这里插入图片描述

2.各位在校同学可以转到自己的实验室,我准备来应聘教职~为中国高等教育做出贡献!

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值