如何在框架外部自定义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. 360加固保so动态脱壳

    环境及工具 手机    : 中兴 U887 系统版本:   Android 2.3.5 工具    :   IDA pro 6.6 .0101Editor 目前so加壳有很多家,我己知的有 爱加密,梆 ...

  2. LA3971组装电脑

    题意:       你有b块钱,想要组装一台电脑,给你提供一些零件,每种零件提供一个或几个,组装电脑的前提是每种零件只能也必须选择一个,每种零件都有自己的种类,名字,价格,还有品质,要求是在能配成电脑 ...

  3. 内核模式下的线程同步的分析(Windows核心编程)

    内核模式下的线程同步 内核模式下的线程同步是用户模式下的线程同步的扩展,因为用户模式下的线程同步有一定的局限性.但用户模式下线程同步的好处是速度快,不需要切换到内核模式(需要额外的 CPU 时间).通 ...

  4. SQLServer的XP_CmdShell提权

    当我们拿到了某个网站SQLServer数据库的SA权限用户密码的话,我们就可以使用XP_CmdShell提权了. 开启xp_cmdshell exec sp_configure 'show advan ...

  5. 【maven】maven资源过滤问题<build>

    需要在pom.xml加入以下代码(在父项目添加即可,如果还不行的话,就在子module的pom.xml也添加) <build> <resources> <resource ...

  6. java.lang.NoSuchMethodError: org.springframework.util.Assert.state(ZLjava/util/function/Supplier;)V

    更多精彩见微信公众号 at org.springframework.test.context.support.AbstractTestContextBootstrapper.buildMergedCo ...

  7. 『政善治』Postman工具 — 8、Postman中Pre-request Script的使用

    目录 1.Pre-request Script介绍 2.常用SNIPPETS(片段)说明 (1)获取变量脚本: (2)设置变量脚本: (3)清空变量脚本: (4)Send a request代码片段 ...

  8. SparkSQL电商用户画像(五)之用户画像开发(客户基本属性表)

    7.电商用户画像开发 7.1用户画像--数据开发的步骤 u 数据开发前置依赖 -需求确定 pv uv topn -建模确定表结构 create table t1(pv int,uv int,topn ...

  9. 剑指offer 数组中的重复数字

    问题描述: 在长度为n的数组中,所有的元素都是0到n-1的范围内. 数组中的某些数字是重复的,但不知道有几个重复的数字,也不知道重复了几次,请找出任意重复的数字. 例如,输入长度为7的数组{2,3,1 ...

  10. [bug] org.apache.catalina.core.StandardContext.startInternal One or more listeners failed to start. Full

    原因 lib文件缺失 参考 https://blog.csdn.net/weixin_41060905/article/details/86911172