如何在框架外部自定义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. 测试工具PerfDog的使用

    使用操作:https://www.jianshu.com/p/cc04c710e643下载地址:https://perfdog.qq.com/

  2. 【Feign】Feign ,OpenFeign以及Ribbon之间的区别?

    Ribbon Ribbon 是 Netflix开源的基于HTTP和TCP等协议负载均衡组件 Ribbon 可以用来做客户端负载均衡,调用注册中心的服务 Ribbon的使用需要代码里手动调用目标服务,请 ...

  3. ASLR 的关闭与开启(适用于 Windows7 及更高版本)

    ASLR 是一种针对缓冲区溢出的安全保护技术,通过对堆.栈.共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术 有的时候 ...

  4. android Javah生成JNI头文件

    项目要用到c语言库,因此来学习下jni 首先是在cmd中使用javah,出现了javah不是内部或外部命令的错误提示,javah是jdk自带的工具,提示说明在系统环境变量中没有jdk的路径,或者配置错 ...

  5. 7 IDEA连接数据库

    IDEA连接数据库 连接成功后,选择数据库 查看数据库/表的内容就双击数据库 修改数据库--要点击DB才能保存 出现问题 错误描述 Server returns invalid timezone. G ...

  6. zTree增加树形菜单格式

    result为json字符串 //展示树形菜单 function showMenuTree(result) { console.log("页面展示函数:"+result); //属 ...

  7. Idea创建Maven Web项目的web.xml版本问题

    问题描述:创建Maven Web项目时,选择MavenWebapp模板时,自动生成的web.xml文件版本为1.4,如图所示 如何才能修改为常用的4.0版本的xml文件呢? 这个文件是从Maven仓库 ...

  8. 【vue-02】基础语法

    插值操作 插值运算符 语法:{{数据}} 插值运算符可以对数据进行显示{{msg}},也可以在插值运算符中进行表达式计算{{cnt*2}}. v-html 希望以html格式进行输出 htmlData ...

  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. 透过“锁”事看InnoDB对并发的处理?

    一. 并发场景下的问题 相对于串行处理方式,并发的事务处理可显著提升数据库的事务吞吐量.提高资源利用率.在MySQL实际应用中,根据场景的不同,可以分为以下几类: 读读并发 读写并发 写写并发 在这些 ...