如何在框架外部自定义C++ OP

通常,如果PaddlePaddle的Operator(OP)库中没有所需要的操作,建议先尝试使用已有的OP组合,如果无法组合出您需要的操作,可以尝试使用paddle.static.py_func,也可以按照这篇教程自定义C++ OP。当然,如果用若干OP组合出来的OP性能无法满足要求,也可以自定义C++ OP。

自定义OP需要以下几个步骤:

  1. 实现OP和注册OP,和在框架内部写OP完全相同,遵守”如何写新的C++ OP”的规范和步骤。当然,实现Gradient OP是可选的。
  2. 编译出动态库。
  3. 封装该OP的Python接口。
  4. 写OP的单测。

下面通过一个具体的例子来详细的介绍,一步一步教会如何实现。下面通过实现relu op来介绍。

自定义OP的实现

OP的实现与”如何写新的C++ OP”的教程相同,简答的说需要: 1). 定义OP的ProtoMaker,即描述OP的输入、输出、属性信息;2). 实现OP的定义和InferShape,以及OP的kernel函数,反向OP类似。3). 注册OP,以及OP的计算函数。

ReLU OP的CPU实现, relu_op.cc 文件:

// relu_op.cc

#include "paddle/fluid/framework/op_registry.h"

namespace paddle {

namespace operators {

// 前向OP的输入X、输出Y、属性

class Relu2OpMaker : public framework::OpProtoAndCheckerMaker {

public:

void Make() override {

AddInput("X", "The input tensor.");

AddOutput("Y", "Output of relu_op");

AddComment(R"DOC(

Relu Operator.

Y = max(X, 0)

)DOC");

}

};

// 前向OP的定义和InferShape实现,设置输出Y的shape

class Relu2Op : public framework::OperatorWithKernel {

public:

using framework::OperatorWithKernel::OperatorWithKernel;

void InferShape(framework::InferShapeContext* ctx) const override {

auto in_dims = ctx->GetInputDim("X");

ctx->SetOutputDim("Y", in_dims);

}

};

// 实现前向OP的Kernel计算函数: Y = max(0, X)

using Tensor = framework::Tensor;

template <typename DeviceContext, typename T>

class Relu2Kernel : public framework::OpKernel<T> {

public:

void Compute(const framework::ExecutionContext& ctx) const override {

auto* in_t = ctx.Input<Tensor>("X");

auto* out_t = ctx.Output<Tensor>("Y");

auto x = in_t->data<T>();

// mutable_data分配内存、获取指针

auto y = out_t->mutable_data<T>(ctx.GetPlace());

for (int i = 0; i < in_t->numel(); ++i) {

y[i] = std::max(static_cast<T>(0.), x[i]);

}

}

};

// 定义反向OP的输入Y和dY、输出dX、属性:

template <typename T>

class Relu2GradMaker : public framework::SingleGradOpMaker<T> {

public:

using framework::SingleGradOpMaker<T>::SingleGradOpMaker;

void Apply(GradOpPtr<T> op) const override {

op->SetType("relu2_grad");

op->SetInput("Y", this->Output("Y"));

op->SetInput(framework::GradVarName("Y"), this->OutputGrad("Y"));

op->SetAttrMap(this->Attrs());

op->SetOutput(framework::GradVarName("X"), this->InputGrad("X"));

}

};

// 定义反向OP和InferShape实现,设置dX的shape

class Relu2GradOp : public framework::OperatorWithKernel {

public:

using framework::OperatorWithKernel::OperatorWithKernel;

void InferShape(framework::InferShapeContext* ctx) const override {

auto in_dims = ctx->GetInputDim(framework::GradVarName("Y"));

ctx->SetOutputDim(framework::GradVarName("X"), in_dims);

}

};

// 实现反向OP的kernel函数 dx = dy * ( y > 0. ? 1. : 0)

template <typename DeviceContext, typename T>

class Relu2GradKernel : public framework::OpKernel<T> {

public:

void Compute(const framework::ExecutionContext& ctx) const override {

auto* dy_t = ctx.Input<Tensor>(framework::GradVarName("Y"));

auto* y_t = ctx.Input<Tensor>("Y");

auto* dx_t = ctx.Output<Tensor>(framework::GradVarName("X"));

auto dy = dy_t->data<T>();

auto y = y_t->data<T>();

auto dx = dx_t->mutable_data<T>(ctx.GetPlace());

for (int i = 0; i < y_t->numel(); ++i) {

dx[i] = dy[i] * (y[i] > static_cast<T>(0) ? 1. : 0.);

}

}

};

// namespace operators

// namespace paddle

namespace ops = paddle::operators;

using CPU = paddle::platform::CPUDeviceContext;

// 注册前向和反向op

// 为了和框架内部的relu区分,这里注册的OP type为relu2

REGISTER_OPERATOR(relu2,

ops::Relu2Op,

ops::Relu2OpMaker,

ops::Relu2GradMaker<paddle::framework::OpDesc>,

ops::Relu2GradMaker<paddle::imperative::OpBase>);

REGISTER_OPERATOR(relu2_grad, ops::Relu2GradOp);

// 注册CPU的Kernel

REGISTER_OP_CPU_KERNEL(relu2,

ops::Relu2Kernel<CPU, float>,

ops::Relu2Kernel<CPU, double>);

REGISTER_OP_CPU_KERNEL(relu2_grad,

ops::Relu2GradKernel<CPU, float>,

ops::Relu2GradKernel<CPU, double>);

ReLU OP的GPU实现, relu_op.cu 文件:

// relu_op.cu

#include "paddle/fluid/framework/op_registry.h"

namespace paddle {

namespace operators {

using Tensor = framework::Tensor;

template <typename T>

__global__ void KeRelu2(const T* x, const int num, T* y) {

int gid = blockIdx.x * blockDim.x + threadIdx.x;

for (int i = gid; i < num; i += blockDim.x * gridDim.x) {

y[i] = max(x[i], static_cast<T>(0.));

}

}

// 前向OP的kernel的GPU实现

template <typename DeviceContext, typename T>

class Relu2CUDAKernel : public framework::OpKernel<T> {

public:

void Compute(const framework::ExecutionContext& ctx) const override {

auto* in_t = ctx.Input<Tensor>("X");

auto* out_t = ctx.Output<Tensor>("Y");

auto x = in_t->data<T>();

auto y = out_t->mutable_data<T>(ctx.GetPlace());

auto& dev_ctx = ctx.template device_context<DeviceContext>();

int num = in_t->numel();

int block = 512;

int grid = (num + block - 1) / block;

KeRelu2<T><<<grid, block, 0, dev_ctx.stream()>>>(x, num, y);

}

};

template <typename T>

__global__ void KeRelu2Grad(const T* y, const T* dy, const int num, T* dx) {

int gid = blockIdx.x * blockDim.x + threadIdx.x;

for (int i = gid; i < num; i += blockDim.x * gridDim.x) {

dx[i] = dy[i] * (y[i] > 0 ? 1. : 0.);

}

}

// 反向OP的kernel的GPU实现

template <typename DeviceContext, typename T>

class Relu2GradCUDAKernel : public framework::OpKernel<T> {

public:

void Compute(const framework::ExecutionContext& ctx) const override {

auto* dy_t = ctx.Input<Tensor>(framework::GradVarName("Y"));

auto* y_t = ctx.Input<Tensor>("Y");

auto* dx_t = ctx.Output<Tensor>(framework::GradVarName("X"));

auto dy = dy_t->data<T>();

auto y = y_t->data<T>();

auto dx = dx_t->mutable_data<T>(ctx.GetPlace());

auto& dev_ctx = ctx.template device_context<DeviceContext>();

int num = dy_t->numel();

int block = 512;

int grid = (num + block - 1) / block;

KeRelu2Grad<T><<<grid, block, 0, dev_ctx.stream()>>>(y, dy, num, dx);

}

};

// namespace operators

// namespace paddle

using CUDA = paddle::platform::CUDADeviceContext;

// 注册前向的GPU Kernel

REGISTER_OP_CUDA_KERNEL(relu2,

paddle::operators::Relu2CUDAKernel<CUDA, float>,

paddle::operators::Relu2CUDAKernel<CUDA, double>);

// 注册反向的GPU Kernel

REGISTER_OP_CUDA_KERNEL(relu2_grad,

paddle::operators::Relu2GradCUDAKernel<CUDA, float>,

paddle::operators::Relu2GradCUDAKernel<CUDA, double>);

注意点:

  1. OP的type不能和PaddlePaddle已有的OP type相同,否则在Python中使用时会报错。

自定义OP的编译

需要将实现的C++、CUDA代码编译成动态库,下面通过g++/nvcc编译,也可以写Makefile或者CMake。

编译需要include PaddlePaddle的相关头文件,如上面代码 paddle/fluid/framework/op_registry.h ,需要链接PaddlePaddle的lib库。 可通过下面命令获取到:

# python

>>> import paddle

>>> print(paddle.sysconfig.get_include())

/paddle/pyenv/local/lib/python2.7/site-packages/paddle/include

>>> print(paddle.sysconfig.get_lib())

/paddle/pyenv/local/lib/python2.7/site-packages/paddle/libs

下面命令可编译出动态库:

include_dir=$( python -c 'import paddle; print(paddle.sysconfig.get_include())' )

lib_dir=$( python -c 'import paddle; print(paddle.sysconfig.get_lib())' )

echo $include_dir

echo $lib_dir

# PaddlePaddel >=1.6.1, 仅需要include ${include_dir} 和 ${include_dir}/third_party

nvcc relu_op.cu -c -o relu_op.cu.o -ccbin cc -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO -DPADDLE_WITH_MKLDNN -Xcompiler -fPIC -std=c++11 -Xcompiler -fPIC -w --expt-relaxed-constexpr -O3 -DNVCC \

-I ${include_dir} \

-I ${include_dir}/third_party \

g++ relu_op.cc relu_op.cu.o -o relu2_op.so -shared -fPIC -std=c++11 -O3 -DPADDLE_WITH_MKLDNN \

-I ${include_dir} \

-I ${include_dir}/third_party \

-L /usr/local/cuda/lib64 \

-L ${lib_dir} -lpaddle_framework -lcudart

注意点:

  1. 通过NVCC编译CUDA源文件时,需要加编译选项 -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO,在框架源码中会使用这些宏定义进行条件编译。用户自定义的C++ OP实现编译时,选项的开启状态需要和核心框架编译行为一致。如EIGEN_USE_GPU是使用Eigen数学库的GPU实现时需要增加的编译选项。
  2. 如果飞桨安装包中不包含MKLDNN库,则需要去掉编译选项-DPADDLE_WITH_MKLDNN。核心框架源码中(比如tensor.h)有使用此宏定义进行条件编译,该选项是否打开同样需要和核心框架编译行为保持一致。默认的飞桨安装包中含有MKLDNN库。
  3. 可多个OP编译到同一个动态库中。
  4. 通过pip方式安装的PaddlePaddle由GCC 4.8编译得到,由于GCC 4.8和GCC 5以上C++11 ABI不兼容,编写的自定义OP,需要通过GCC 4.8编译。若是GCC 5及以上的环境上使用自定义OP,推荐使用Docker安装PaddlePaddle,使得编Paddle和编译自定义OP的GCC版本相同。

封装Python Layer接口

需要使用 paddle.incubate.load_op_library 接口调用加载动态库,使得PaddlePaddle的主进程中可以使用用户自定义的OP。

# custom_op.py

import paddle.incubate as incubate

# 调用load_op_library加载动态库

incubate.load_op_library('relu2_op.so')

from paddle.incubate import LayerHelper

def relu2(x, name=None):

# relu2的type和在OP中定义的type相同

helper = LayerHelper("relu2", **locals())

# 创建输出Variable

out = helper.create_variable_for_type_inference(dtype=x.dtype)

helper.append_op(type="relu2", inputs={"X": x}, outputs={"Y": out})

return out

注意点:

  1. 一个动态库只需使用paddle.incubate.load_op_library在paddle import之后加载一次即可。
  2. Python接口的封装和PaddlePaddle框架内部的封装相同,更多的示例也可以阅读源码中 python/paddle/fluid/layers/nn.py的代码示例。

单测测试

可以写个简单的Python程序测试计算的正确性:

静态图模式

import numpy as np

import paddle

from custom_op import relu2

paddle.enable_static()

data = paddle.static.data(name='data', shape=[None, 32], dtype='float32')

relu = relu2(data)

use_gpu = True  # or False

paddle.set_device('gpu' if use_gpu else 'cpu')

exe = paddle.static.Executor()

x = np.random.uniform(-1, 1, [4, 32]).astype('float32')

out, = exe.run(feed={'data': x}, fetch_list=[relu])

np.allclose(out, np.maximum(x, 0.))

动态图模式

import numpy as np

import paddle

from custom_op import relu2

use_gpu = True  # or False

paddle.set_device('gpu' if use_gpu else 'cpu')

x = np.random.uniform(-1, 1, [4, 32]).astype('float32')

t = paddle.to_tensor(x)

out = relu2(t)

np.allclose(out.numpy(), np.maximum(x, 0.))

接下来可以在模型中使用您自定义的OP了!

如何在C++预测库中使用

暂时不支持在C++预测库中使用,后续会补充在C++预测库中的使用示例。

FAQ

  1. Q: 如果出现类似错误: relu2_op.so: cannot open shared object file: No such file or directory 以及 libpaddle_framework.so: cannot open shared object file: No such file or directory。

A: 需要将relu2_op.so所在路径以及libpaddle_framework.so路径(即paddle.sysconfig.get_lib()得到路径)设置到环境变量LD_LIBRARY_PATH中:

# 假如relu2_op.so路径是:`paddle/test`,对于Linux环境设置:

export LD_LIBRARY_PATH=paddle/test:$( python -c 'import paddle; print(paddle.sysconfig.get_lib())'):$LD_LIBRARY_PATH

如何在框架外部自定义C++ OP的更多相关文章

  1. AI框架外部用户贡献代码

    AI框架外部用户贡献代码 概述 飞桨是百度自主研发的一款开源的深度学习框架,是主流深度学习框架中首个完全国产化的产品,已经在农业.医疗.林业.科研.服务等领域成功应用.无论是已入职场的深度学习从业者. ...

  2. ThinkPHP框架配置自定义的模板变量(十)

    原文:ThinkPHP框架配置自定义的模板变量(十) 模板替换(手册有详细介绍对应的目录) __PUBLIC__:会被替换成当前网站的公共目录 通常是 /Public/ __ROOT__: 会替换成当 ...

  3. 仿百度壁纸客户端(一)——主框架搭建,自定义Tab+ViewPager+Fragment

    仿百度壁纸客户端(一)--主框架搭建,自定义Tab+ViewPager+Fragment 百度壁纸系列 仿百度壁纸客户端(一)--主框架搭建,自定义Tab + ViewPager + Fragment ...

  4. Thinkphp框架中自定义修改success和error页面

    Thinkphp框架中自定义修改success和error页面 Thinkphp框架的默认success和error太难看,可以自定义设置,步骤如下: (注意:TP原框架中的success跳转有问题, ...

  5. 第三百一十四节,Django框架,自定义分页

    第三百一十四节,Django框架,自定义分页 自定义分页模块 #!/usr/bin/env python #coding:utf-8 from django.utils.safestring impo ...

  6. unity3d MonoDevelop引用外部自定义dll文件报错:are you missing an assembly reference?

    在unity3d 编辑器 MonoDevelop 中引用外部自定义dll文件报错:are you missing an assembly reference? 因为unity还停留在.NET Fram ...

  7. Java集合框架实现自定义排序

    Java集合框架针对不同的数据结构提供了多种排序的方法,虽然很多时候我们可以自己实现排序,比如数组等,但是灵活的使用JDK提供的排序方法,可以提高开发效率,而且通常JDK的实现要比自己造的轮子性能更优 ...

  8. CI框架中自定义view文件夹位置

    要想自定义view文件夹的位置,首先要了解CI框架时如何加载view文件夹的. CI中默认调用view的方法是: $this->load->view(); //这一行代码的原理是什么呢?请 ...

  9. Javascript框架的自定义事件(转)

    很多 javascript 框架都提供了自定义事件(custom events),例如 jquery.yui 以及 dojo 都支持“document ready”事件.而部分自定义事件是源自回调(c ...

随机推荐

  1. 13- jmeter性能测试案例

    配置原件 HTTP请求默认值 前置处理程序 定时器 取样器 后置处理器:正则表达式提取器 断言 监听器 性能测试流程 1.评估获取性能测试需求(访问量大,操作频繁) 2.确定性能测试目标 : 并发用户 ...

  2. 基于Docker安装的MindSpore-1.2 GPU版本

    技术背景 在前面一篇博客中,我们介绍过MindSpore-CPU版本的Docker部署以及简单的案例测试,当时官方还不支持GPU版本的Docker容器化部署.经过MindSpore团队的努力,1.2. ...

  3. js收藏展开与隐藏,返回顶部

    var a = document.getElementById("more");var b = document.getElementById("moreList&quo ...

  4. Appium命令行启动,提示找不到命令,本地没有appium.cmd文件

    安装appium时,直接从github上下载的appium-desktop-windows版本,安装后,从打开桌面端Server,能启动服务,appium-doctor也能正常运行. 但奇怪的地方来了 ...

  5. 将这段美化的css代码

    很多时候如果不是用了很多样式,很难把边框修饰得好看,看了一篇博文,觉得真的挺漂亮,也挺好看. 转载的博文地址 将这段美化的css代码 border:1px solid #96c2f1;backgrou ...

  6. 一种Maven项目启动时不编译java文件的解决方案

    问题 前提介绍 : 环境版本 : JDK -version : 1.8.0-251 Tomcat -version : 8.5.5 Maven -version : 3.6.3 项目情况描述 使用ID ...

  7. typecho+宝塔搭建

    在这个互动视频中很详细的讲解到了 https://www.bilibili.com/video/BV1o4411r7x5?spm_id_from=pageDriver

  8. web自动化框架—BasePage 类的简单封装

    优秀的框架都有属于自己的思想,在搭建web自动化测试框架时,我们通常都遵循 PO(Page Object)思想. 简单理解就是我们会把每个页面看成一个对象,一切皆对象,面向对象编码,这样会让我们更好的 ...

  9. UVA OJ 623 500!

    500!  In these days you can more and more often happen to see programs which perform some useful cal ...

  10. input type

    input的type有: text 文本输入 password密码输入 file选择文件 radio单选按钮 checkbox复选按钮 submit对应form的action按钮 button 普通按 ...