PyTorch中的C++扩展实现

今天要聊聊用 PyTorch 进行 C++ 扩展。

在正式开始前,我们需要了解 PyTorch 如何自定义module。这其中,最常见的就是在 python 中继承torch.nn.Module,用 PyTorch 中已有的 operator 来组装成自己的模块。这种方式实现简单,但是,计算效率却未必最佳,另外,如果我们想实现的功能过于复杂,可能 PyTorch 中那些已有的函数也没法满足我们的要求。这时,用 C、C++、CUDA 来扩展 PyTorch 的模块就是最佳的选择了。

由于目前市面上大部分深度学习系统(TensorFlow、PyTorch 等)都是基于 C、C++ 构建的后端,因此这些系统基本都存在 C、C++ 的扩展接口。PyTorch 是基于 Torch 构建的,而 Torch 底层采用的是 C 语言,因此 PyTorch 天生就和 C 兼容,因此用 C 来扩展 PyTorch 并非难事。而随着 PyTorch1.0 的发布,官方已经开始考虑将 PyTorch 的底层代码用 caffe2 替换,因此他们也在逐步重构 ATen,后者是目前 PyTorch 使用的 C++ 扩展库。总的来说,C++ 是未来的趋势。至于 CUDA,这是几乎所有深度学习系统在构建之初就采用的工具,因此 CUDA 的扩展接口是标配。

本文用一个简单的例子,梳理一下进行 C++ 扩展的步骤,至于一些具体的实现,不做深入探讨。

PyTorch的C、C++、CUDA扩展

关于 PyTorch 的 C 扩展,可以参考官方教程或者这篇博文,其操作并不难,无非是借助原先 Torch 提供的<TH/TH.h><THC/THC.h>等接口,再利用 PyTorch 中提供的torch.util.ffi模块进行扩展。需要注意的是,随着 PyTorch 版本升级,这种做法在新版本的 PyTorch 中可能会失效。

本文主要介绍 C++(未来可能加上 CUDA)的扩展方法。

C++扩展

首先,介绍一下基本流程。在 PyTorch 中扩展 C++/CUDA 主要分为几步:

  1. 安装好 pybind11 模块(通过 pip 或者 conda 等安装),这个模块会负责 python 和 C++ 之间的绑定;
  2. 用 C++ 写好自定义层的功能,包括前向传播forward和反向传播backward;
  3. 写好 setup.py,并用 python 提供的setuptools来编译并加载 C++ 代码。
  4. 编译安装,在 python 中调用 C++ 扩展接口。

接下来,我们就用一个简单的例子(z=2x+y)来演示这几个步骤。

第一步

安装 pybind11 比较简单,直接略过。我们先写好 C++ 相关的文件:

头文件 test.h

#include <torch/extension.h>
#include <vector>

// 前向传播
torch::Tensor Test_forward_cpu(const torch::Tensor& inputA,
              const torch::Tensor& inputB);
// 反向传播
std::vector<torch::Tensor> Test_backward_cpu(const torch::Tensor& gradOutput);

注意,这里引用的<torch/extension.h>头文件至关重要,它主要包括三个重要模块:

  • pybind11,用于 C++ 和 python 交互;
  • ATen,包含 Tensor 等重要的函数和类;
  • 一些辅助的头文件,用于实现 ATen 和 pybind11 之间的交互。

源文件 test.cpp 如下:

#include "test.h"

// 前向传播,两个 Tensor 相加。这里只关注 C++ 扩展的流程,具体实现不深入探讨。
torch::Tensor Test_forward_cpu(const torch::Tensor& x,
              const torch::Tensor& y) {
  AT_ASSERTM(x.sizes() == y.sizes(), "x must be the same size as y");
  torch::Tensor z = torch::zeros(x.sizes());
  z = 2 * x + y;
  return z;
}

// 反向传播
// 在这个例子中,z对x的导数是2,z对y的导数是1。
// 至于这个backward函数的接口(参数,返回值)为何要这样设计,后面会讲。
std::vector<torch::Tensor> Test_backward_cpu(const torch::Tensor& gradOutput) {
  torch::Tensor gradOutputX = 2 * gradOutput * torch::ones(gradOutput.sizes());
  torch::Tensor gradOutputY = gradOutput * torch::ones(gradOutput.sizes());
  return {gradOutputX, gradOutputY};
}

// pybind11 绑定
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
 m.def("forward", &Test_forward_cpu, "TEST forward");
 m.def("backward", &Test_backward_cpu, "TEST backward");
}

第二步

新建一个编译安装的配置文件 setup.py,文件目录安排如下:

└── csrc
  ├── cpu
  │  ├── test.cpp
  │  └── test.h
  └── setup.py

以下是 setup.py 中的内容:

from setuptools import setup
import os
import glob
from torch.utils.cpp_extension import BuildExtension, CppExtension

# 头文件目录
include_dirs = os.path.dirname(os.path.abspath(__file__))
# 源代码目录
source_cpu = glob.glob(os.path.join(include_dirs, 'cpu', '*.cpp'))

setup(
  name='test_cpp', # 模块名称,需要在python中调用
  version="0.1",
  ext_modules=[
    CppExtension('test_cpp', sources=source_cpu, include_dirs=[include_dirs]),
  ],
  cmdclass={
    'build_ext': BuildExtension
  }
)

注意,这个 C++ 扩展被命名为test_cpp,意思是说,在 python 中可以通过test_cpp模块来调用 C++ 函数。

第三步

在 cpu 这个目录下,执行下面的命令编译安装 C++ 代码:

python setup.py install

之后,可以看到一堆输出,该 C++ 模块会被安装在 python 的 site-packages 中。

完成上面几步后,就可以在 python 中调用 C++ 代码了。在 PyTorch 中,按照惯例需要先把 C++ 中的前向传播和反向传播封装成一个函数op(以下代码放在 test.py 文件中):

from torch.autograd import Function

import test_cpp

class TestFunction(Function):

  @staticmethod
  def forward(ctx, x, y):
    return test_cpp.forward(x, y)

  @staticmethod
  def backward(ctx, gradOutput):
    gradX, gradY = test_cpp.backward(gradOutput)
    return gradX, gradY

这样一来,我们相当于把 C++ 扩展的函数嵌入到 PyTorch 自己的框架内。

我查看了这个Function类的代码,发现是个挺有意思的东西:

class Function(with_metaclass(FunctionMeta, _C._FunctionBase, _ContextMethodMixin, _HookMixin)):

  ...

  @staticmethod
  def forward(ctx, *args, **kwargs):
    r"""Performs the operation.

    This function is to be overridden by all subclasses.

    It must accept a context ctx as the first argument, followed by any
    number of arguments (tensors or other types).

    The context can be used to store tensors that can be then retrieved
    during the backward pass.
    """
    raise NotImplementedError

  @staticmethod
  def backward(ctx, *grad_outputs):
    r"""Defines a formula for differentiating the operation.

    This function is to be overridden by all subclasses.

    It must accept a context :attr:`ctx` as the first argument, followed by
    as many outputs did :func:`forward` return, and it should return as many
    tensors, as there were inputs to :func:`forward`. Each argument is the
    gradient w.r.t the given output, and each returned value should be the
    gradient w.r.t. the corresponding input.

    The context can be used to retrieve tensors saved during the forward
    pass. It also has an attribute :attr:`ctx.needs_input_grad` as a tuple
    of booleans representing whether each input needs gradient. E.g.,
    :func:`backward` will have ``ctx.needs_input_grad[0] = True`` if the
    first input to :func:`forward` needs gradient computated w.r.t. the
    output.
    """
    raise NotImplementedError

这里需要注意一下backward的实现规则。该接口包含两个参数:ctx是一个辅助的环境变量,grad_outputs则是来自前一层网络的梯度列表,而且这个梯度列表的数量与forward函数返回的参数数量相同,这也符合链式法则的原理,因为链式法则就需要把前一层中所有相关的梯度与当前层进行相乘或相加。同时,backward需要返回forward中每个输入参数的梯度,如果forward中包括 n 个参数,就需要一一返回 n 个梯度。所以,在上面这个例子中,我们的backward函数接收一个参数作为输入(forward只输出一个变量),并返回两个梯度(forward接收上一层两个输入变量)。

定义完Function后,就可以在Module中使用这个自定义op了:

import torch

class Test(torch.nn.Module):

  def __init__(self):
    super(Test, self).__init__()

  def forward(self, inputA, inputB):
    return TestFunction.apply(inputA, inputB)

现在,我们的文件目录变成:

├── csrc
│  ├── cpu
│  │  ├── test.cpp
│  │  └── test.h
│  └── setup.py
└── test.py

之后,我们就可以将 test.py 当作一般的 PyTorch 模块进行调用了。

测试

下面,我们测试一下前向传播和反向传播:

import torch
from torch.autograd import Variable

from test import Test

x = Variable(torch.Tensor([1,2,3]), requires_grad=True)
y = Variable(torch.Tensor([4,5,6]), requires_grad=True)
test = Test()
z = test(x, y)
z.sum().backward()
print('x: ', x)
print('y: ', y)
print('z: ', z)
print('x.grad: ', x.grad)
print('y.grad: ', y.grad)

输出如下:

x:  tensor([1., 2., 3.], requires_grad=True)
y:  tensor([4., 5., 6.], requires_grad=True)
z:  tensor([ 6.,  9., 12.], grad_fn=<TestFunctionBackward>)
x.grad:  tensor([2., 2., 2.])
y.grad:  tensor([1., 1., 1.])

可以看出,前向传播满足 z=2x+y,而反向传播的结果也在意料之中。

CUDA扩展

虽然 C++ 写的代码可以直接跑在 GPU 上,但它的性能还是比不上直接用 CUDA 编写的代码,毕竟 ATen 没法并不知道如何去优化算法的性能。不过,由于我对 CUDA 仍一窍不通,因此这一步只能暂时略过,留待之后补充~囧~。

参考

CUSTOM C EXTENSIONS FOR PYTORCH
CUSTOM C++ AND CUDA EXTENSIONS
Pytorch拓展进阶(一):Pytorch结合C以及Cuda语言
Pytorch拓展进阶(二):Pytorch结合C++以及Cuda拓展

到此这篇关于PyTorch中的C++扩展实现的文章就介绍到这了,更多相关PyTorch C++扩展 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • pytorch中使用cuda扩展的实现示例

    以下面这个例子作为教程,实现功能是element-wise add: (pytorch中想调用cuda模块,还是用另外使用C编写接口脚本) 第一步:cuda编程的源文件和头文件 // mathutil_cuda_kernel.cu // 头文件,最后一个是cuda特有的 #include <curand.h> #include <stdio.h> #include <math.h> #include <float.h> #include "math

  • PyTorch中的C++扩展实现

    今天要聊聊用 PyTorch 进行 C++ 扩展. 在正式开始前,我们需要了解 PyTorch 如何自定义module.这其中,最常见的就是在 python 中继承torch.nn.Module,用 PyTorch 中已有的 operator 来组装成自己的模块.这种方式实现简单,但是,计算效率却未必最佳,另外,如果我们想实现的功能过于复杂,可能 PyTorch 中那些已有的函数也没法满足我们的要求.这时,用 C.C++.CUDA 来扩展 PyTorch 的模块就是最佳的选择了. 由于目前市面上

  • PyTorch中clone()、detach()及相关扩展详解

    clone() 与 detach() 对比 Torch 为了提高速度,向量或是矩阵的赋值是指向同一内存的,这不同于 Matlab.如果需要保存旧的tensor即需要开辟新的存储地址而不是引用,可以用 clone() 进行深拷贝, 首先我们来打印出来clone()操作后的数据类型定义变化: (1). 简单打印类型 import torch a = torch.tensor(1.0, requires_grad=True) b = a.clone() c = a.detach() a.data *=

  • PyTorch中的Variable变量详解

    一.了解Variable 顾名思义,Variable就是 变量 的意思.实质上也就是可以变化的量,区别于int变量,它是一种可以变化的变量,这正好就符合了反向传播,参数更新的属性. 具体来说,在pytorch中的Variable就是一个存放会变化值的地理位置,里面的值会不停发生片花,就像一个装鸡蛋的篮子,鸡蛋数会不断发生变化.那谁是里面的鸡蛋呢,自然就是pytorch中的tensor了.(也就是说,pytorch都是有tensor计算的,而tensor里面的参数都是Variable的形式).如果

  • 在Pytorch中使用Mask R-CNN进行实例分割操作

    在这篇文章中,我们将讨论mask R-CNN背后的一些理论,以及如何在PyTorch中使用预训练的mask R-CNN模型. 1.语义分割.目标检测和实例分割 之前已经介绍过: 1.语义分割:在语义分割中,我们分配一个类标签(例如.狗.猫.人.背景等)对图像中的每个像素. 2.目标检测:在目标检测中,我们将类标签分配给包含对象的包围框. 一个非常自然的想法是把两者结合起来.我们只想在一个对象周围识别一个包围框,并且找到包围框中的哪些像素属于对象. 换句话说,我们想要一个掩码,它指示(使用颜色或灰

  • PyTorch 中的傅里叶卷积实现示例

    卷积 卷积在数据分析中无处不在.几十年来,它们一直被用于信号和图像处理.最近,它们成为现代神经网络的重要组成部分.如果你处理数据的话,你可能会遇到错综复杂的问题. 数学上,卷积表示为: 尽管离散卷积在计算应用程序中更为常见,但在本文的大部分内容中我将使用连续形式,因为使用连续变量来证明卷积定理(下面讨论)要容易得多.之后,我们将回到离散情况,并使用傅立叶变换在 PyTorch 中实现它.离散卷积可以看作是连续卷积的近似,其中连续函数离散在规则网格上.因此,我们不会为这个离散的案例重新证明卷积定理

  • 对Pytorch 中的contiguous理解说明

    最近遇到这个函数,但查的中文博客里的解释貌似不是很到位,这里翻译一下stackoverflow上的回答并加上自己的理解. 在pytorch中,只有很少几个操作是不改变tensor的内容本身,而只是重新定义下标与元素的对应关系的.换句话说,这种操作不进行数据拷贝和数据的改变,变的是元数据. 这些操作是: narrow(),view(),expand()和transpose() 举个栗子,在使用transpose()进行转置操作时,pytorch并不会创建新的.转置后的tensor,而是修改了ten

  • pytorch中的nn.ZeroPad2d()零填充函数实例详解

    在卷积神经网络中,有使用设置padding的参数,配合卷积步长,可以使得卷积后的特征图尺寸大小不发生改变,那么在手动实现图片或特征图的边界零填充时,常用的函数是nn.ZeroPad2d(),可以指定tensor的四个方向上的填充,比如左边添加1dim.右边添加2dim.上边添加3dim.下边添加4dim,即指定paddin参数为(1,2,3,4),本文中代码设置的是(3,4,5,6)如下: import torch.nn as nn import cv2 import torchvision f

  • pytorch中DataLoader()过程中遇到的一些问题

    如下所示: RuntimeError: stack expects each tensor to be equal size, but got [3, 60, 32] at entry 0 and [3, 54, 32] at entry 2 train_dataset = datasets.ImageFolder( traindir, transforms.Compose([ transforms.Resize((224)) ### 原因是 transforms.Resize() 的参数设置问

  • pytorch中的广播语义

    目录 1.什么是广播语义? 2.广播语义的规则 3.不符合广播语义的例子 4.符合广播语义的例子 pytorch的广播语义(broadcasting semantics),和numpy的很像,所以可以先看看numpy的文档: 1.什么是广播语义? 官方文档有这样一个解释: In short, if a PyTorch operation supports broadcast, then its Tensor arguments can be automatically expanded to b

随机推荐