AI框架外部用户贡献代码
概述
飞桨是百度自主研发的一款开源的深度学习框架,是主流深度学习框架中首个完全国产化的产品,已经在农业、医疗、林业、科研、服务等领域成功应用。无论是已入职场的深度学习从业者、爱好者,亦或是在校学生,百度飞桨非常欢迎大家能够在开源生态Github中贡献代码,与实时分享项目的成功应用和的奇思妙想。贡献的代码可以是模型、框架的算子、框架新增功能或者飞桨平台优化建议等。一旦贡献的代码被飞桨接受,将有机会让更多的深度学习用户受益。同时,为了促进深度学习快速发展和应用,飞桨会定期组织优秀代码展播和表彰等活动,可以随时关注飞桨官网了解更详细的信息。
深度学习的应用触及各行各业,再强大的模型库也无法完全匹配业务需求,往往需要结合业务和数据特点进行参数调优。因此,在使用飞桨进行模型开发时,需要经常与算子操作打交道。如果飞桨提供的算子(Operator, 简称OP)无法满足模型需求,可以使用自定义Python算子和自定义C++算子的功能编写。本文主要介绍飞桨算子的写法以及在GitHub上贡献代码的操作方法。
自定义算子
飞桨支持Python和C++两种类型算子,建议使用场景如下:
- Python算子: 在模型实现的过程中,如果遇到缺失算子,建议先尝试使用已有的算子进行组合。如果已有的算子无法组合出需要的操作,可通过自定义Python算子的功能开发新的算子。
- C++算子: 如果用若干算子组合出的算子在性能上无法满足要求时,可以通过自定义C++算子提升性能。
自定义Python算子
飞桨通过 paddle.static.py_func 接口在Python端编写算子,先了解下py_func接口。接口格式如下:
def py_func(func, x, out, backward_func=None, skip_vars_in_backward_input=None)
- func : 是前向计算函数。在运行网络前向时,飞桨会调用 out = func(*x) ,根据前向输入 x 和前向函数 func 计算前向输出 out。
- x : 是输入变量,可以是单个 Tensor 、 List[Tensor]或者tuple(Tensor)。
- out : 是输出变量,可以是单个 Tensor 或者 List[Tensore] 。
- backward_func : 是反向计算函数。若 backward_func 为 None ,则该Python Op没有反向计算逻辑;若 backward_func 不为 None,则飞桨会在运行网络反向时调用 backward_func 计算前向输入 x 的梯度。
- skip_vars_in_backward_input: 为backward_func 中不需要的输入,可以是单个 Tensor 或者 List[Tensor] 。
Python算子由计算方法、输入变量和输出变量组成。一般来讲,通过飞桨paddle.static.py_func接口进行Python算子开发需要如下三步:
- 定义计算方法:定义前向函数和反向函数。
- 定义算子输出变量:创建前向输出变量。
- 调用算子组网:使用 paddle.static.py_func组建网络。
1. 定义计算方法
定义前向函数
若前向函数的输入为 x_1, x_2, …, x_n ,输出为y_1, y_2, …, y_m,则定义前向函数的格式为:
def foward_func(x_1, x_2, ..., x_n):
...
return y_1, y_2, ..., y_m
例如:定义relu的前向函数,代码如下:
def relu(x):
return np.maximum(x, 0.0)
定义反向函数
默认情况下,反向函数的输入参数顺序为: 所有前向输入变量 + 所有前向输出变量 + 所有前向输出变量的梯度,因此定义反向函数的格式为:
def backward_func(x_1, x_2, ..., x_n, y_1, y_2, ..., y_m, dy_1, dy_2, ..., dy_m):
...
return dx_1, dx_2, ..., dx_n
例如:定义relu的方向函数,代码如下:
def relu_grad(y, dy):
dx = np.zeros_like(y)
dx[y > 0] = 1.0
return np.array(dy) * dx
说明:
- 若反向函数不需要某些前向输入变量或前向输出变量,可通过 skip_vars_in_backward_input 参数进行设置。
- 前向函数和反向函数的输入均是 Tensor,输出是Tensor或numpy.array。Tensor和numpy.array可以相互转换。
2. 定义算子输出变量
飞桨通过Program.current_block().create_var 创建前向输出变量,其中变量的名称name、数据类型dtype和维度shape为必选参数,格式如下:
import paddle
def create_tmp_var(program, name, dtype, shape):
return program.current_block().create_var(name=name, dtype=dtype, shape=shape)
# 开启静态图
paddle.enable_static()
# 手动创建前向输出变量
y_var = create_tmp_var(paddle.static.default_main_program(), 'output', 'float32', [-1, 1, 28, 28])
print(y_var)
3. 调用算子组网
在组网过程中,通过调用paddle.static.py_func将定义好的Python计算函数加入整个模型中,代码如下:
x = paddle.static.data(name='x', shape=[-1,1,28,28], dtype='int32')
paddle.static.py_func(func=relu, x=x, out=y_var, backward_func=relu_grad, skip_vars_in_backward_input=x)
说明:
- 若在反向函数输入参数中不希望出现前向输入,可通过 skip_vars_in_backward_input 参数进行设置。
- skip_vars_in_backward_input 只能跳过前向输入变量和前向输出变量,不能跳过前向输出的梯度。
- py_func 的前向函数和反向函数中不应调用fluid.layers里的操作,原因如下:
- fluid.layers里的操作是在组建网络阶段调用,输入参数为Python端的 Variable 。
- 前向函数和反向函数是在网络运行时调用,且输入参数均为C++端的 LoDTensor 。
- 若某个前向输出变量没有梯度,则 backward_func 的输入为 None 。若某个前向输入变量没有梯度,则 backward_func 的输出为None。
自定义Python算子实例
下面通过三个不同场景的实例,具体介绍Python算子的实现方法和注意事项。
实例1: 实现z=x + y的算子
import numpy as np
import paddle
paddle.enable_static()
#第一步: 定义加法的前向函数
def element_wise_add(x, y):
x = np.array(x)
y = np.array(y)
if x.shape != y.shape:
raise AssertionError("the shape of inputs must be the same!")
z = x + y
return z
def create_tmp_var(name, dtype, shape):
return paddle.static.default_main_program().current_block().create_var(
name=name, dtype=dtype, shape=shape)
x = paddle.static.data(name='x1', shape=[2, 3], dtype='int32')
y = paddle.static.data(name='y1', shape=[2, 3], dtype='int32')
#第二步: 创建前向输出变量, name是'output', 数据类型是'int32', 形状是[2,3]
output = create_tmp_var('output','int32', [2, 3])
#第三步: 使用py_func组建网络,设置前向计算函数,输入是[x,y]2个变量,输出是output
paddle.static.py_func(func=element_wise_add, x=[x, y], out=output)
exe=paddle.static.Executor(paddle.CPUPlace())
exe.run(paddle.static.default_startup_program())
x_arr = np.random.randint(1, 10, size=[2, 3], dtype='int32')
y_arr = np.random.randint(1, 10, size=[2, 3], dtype='int32')
out = exe.run(feed={'x1':x_arr, 'y1':y_arr},
fetch_list=[output])
print("{0} + \n{1} = \n{2}".format(x_arr, y_arr, out[0]))
[[7 6 2]
[7 9 2]] +
[[6 9 2]
[2 5 7]] =
[[13 15 4]
[ 9 14 9]]
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/nn.py:13402: DeprecationWarning: inspect.getargspec() is deprecated since Python 3.0, use inspect.signature() or inspect.getfullargspec()
args = inspect.getargspec(self._func)
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/executor.py:1150: UserWarning: There are no operators in the program to be executed. If you pass Program manually, please use fluid.program_guard to ensure the current Program is being used.
warnings.warn(error_info)
实例2:实现relu构建神经网络的算子
import numpy as np
import paddle
#第一步: 定义前向和反向函数
#前向函数1个输入x,一个输出y=np.maximum(x, 0.0)
def relu(x):
return np.maximum(x, 0.0)
#反向函数2个输入:前向的输出y和y的梯度;一个输出:前向输入的梯度
def relu_grad(y, dy):
y = np.array(y)
dx = np.zeros_like(y)
dx[y > 0] = 1.0
return np.array(dy) * dx
def create_tmp_var(name, dtype, shape):
return paddle.static.default_main_program().current_block().create_var(
name=name, dtype=dtype, shape=shape)
# 开启静态图模式
paddle.enable_static()
x = paddle.static.data(name='x2', shape=[None, 16], dtype='float32')
y = paddle.static.data(name='y2', shape=[None, 1], dtype='int64')
fc = paddle.static.nn.fc(x, size=200)
#第二步:创建前向输出的变量,name是relu,数据类型和fc保持一致,shape和fc保持一致
act_var = create_tmp_var(name='relu', dtype=fc.dtype, shape=fc.shape)
#第三步:使用py_func组建网络,设置前向、反向计算函数,输入是fc,输出是act_var
act = paddle.static.py_func(func=relu, x=fc,
out=act_var, backward_func=relu_grad,
skip_vars_in_backward_input=fc)
prediction = paddle.static.nn.fc(act, size=10)
loss = paddle.nn.functional.cross_entropy(input=prediction, label=y)
loss = paddle.mean(loss)
optimizer = paddle.optimizer.SGD(learning_rate=0.001)
optimizer.minimize(loss)
exe = paddle.static.Executor(paddle.CPUPlace())
exe.run(paddle.static.default_startup_program())
x_arr = np.random.random(size=(1, 16)).astype('float32')
y_arr = np.random.randint(0, 10, size=[1, 1], dtype='int64')
out = exe.run(feed={'x2':x_arr, 'y2':y_arr},
fetch_list=[loss])
print(out[0])
实例3:实现输入输出为LoDTensor类型的算子
import numpy as np
import paddle
def scale(x):
seq_len = x.recursive_sequence_lengths()
x = np.array(x) * 2.
y = paddle.fluid.LoDTensor()
y.set(x, paddle.CPUPlace())
y.set_recursive_sequence_lengths(seq_len)
print(seq_len)
return y
def create_tmp_var(name, dtype, shape):
return paddle.static.default_main_program().current_block().create_var(
name=name, dtype=dtype, shape=shape)
# 开启静态图
paddle.enable_static()
x = paddle.static.data(name='x', shape=[5, 4], dtype='float32', lod_level=1)
output = create_tmp_var('output','float32', [5, 4])
paddle.static.py_func(func=scale, x=[x], out=output)
place = paddle.CPUPlace()
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
x_arr = np.random.random(size=(5, 4)).astype('float32')
x_t = paddle.fluid.create_lod_tensor(x_arr, [[2, 3]], place)
out = exe.run(feed={'x':x_t},
fetch_list=[output],
return_numpy=False)
print("{0} * 2. = \n{1}\nsequence_length: {2}".format(x_arr,
np.array(out[0]), out[0].recursive_sequence_lengths()))
自定义C++算子
自定义C++算子的实现形式和飞桨框架里实现算子形式相同,用户在框架外部自定义算子,需要如下四步:
- 实现算子:算子的实现和注册需要遵守飞桨写新C++ OP的规范和步骤,其中实现反向OP为可选操作。
- 编译算子:编译生成动态链接库,目的是为了主程序运行时自动加载该动态库,获得编译时的算子定义,以及运行时调用此动态库中算子实现。
- 封装Python Layer接口:封装成Python Layer接口,以便搭建模型组网时调用。
- 单元测试:通过单元测试验证代码实现的正确性。
1. 实现C++算子
下面以实现relu算子为例,介绍实现C++算子的具体步骤。ReLU OP的实现有两种部署场景,CPU和GPU,用户可以按照实际业务需求,选择对应的实现方法。
场景一:ReLU OP的CPU实现
#include "paddle/fluid/framework/op_registry.h"
namespace paddle {
namespace operators {
// 1. 定义前向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");
}
};
// 1) 定义前向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);
}
};
// 2) 实现前向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]);
}
}
};
// 2. 定义反向OP的输入Y和dY、输出dX、属性。如果不需要反向OP,此步骤可忽略。
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"));
}
};
// 1) 定义反向OP和InferShape实现,设置dX的shape。如果不需要反向OP,此步骤可忽略。
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);
}
};
// 2) 实现反向OP的kernel函数 dx = dy * ( y > 0. ? 1. : 0)如果不需要反向OP,此步骤可忽略。
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;
// 3. 注册前向和反向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
#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>);
注意:
OP的type不能和飞桨已有的OP type相同,否则在Python中使用时会报错。
2. 编译算子
实现C++算子之后,可以通过编译算子的方式生成动态链接库,以便后续封装算子的Python Layer接口,供模型搭建组网以及运行时执行引擎时调用。对于C++算子的编译有两种方式:G++命令行编译、CMake工具编译。下面以命令行的编译方式为例,介绍编译算子的方法。
说明:
如果实现算子中包含CUDA程序,需要做如下处理:
- 通过NVCC对CUDA源文件(通常是.cu文件)进行编译,产生目标文件。
- 通过G++命令将NVCC编译产生的目标文件和C++源文件编译生成动态链接库。
算子实现中添加了飞桨核心框架的头文件引用,编译时需要链接核心框架动态库。通过paddle.sysconfig.get_include()和paddle.sysconfig.get_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
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
说明:
- 通过NVCC编译CUDA源文件时,需要加编译选项 -DPADDLE_WITH_CUDA -DEIGEN_USE_GPU -DPADDLE_USE_DSO,在框架源码中会使用这些宏定义进行条件编译。用户自定义的C++ OP实现编译时,选项的开启状态需要和核心框架编译行为一致。如EIGEN_USE_GPU是使用Eigen数学库的GPU实现时需要增加的编译选项。
- 如果飞桨安装包中不包含MKLDNN库,则需要去掉编译选项-DPADDLE_WITH_MKLDNN。核心框架源码中(比如tensor.h)有使用此宏定义进行条件编译,该选项是否打开同样需要和核心框架编译行为保持一致。默认的飞桨安装包中含有MKLDNN库。
- 支持将多个OP编译到同一个动态库中。
- 通过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版本相同。
3. 封装Python Layer接口
通过 load_op_library 接口加载动态库,在飞桨的主进程中执行用户自定义的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
说明:
- 一个动态库只需使用paddle.incubate.load_op_library加载一次即可。
- Python接口的封装和PaddlePaddle框架内部的封装相同,更多的示例也可以阅读源码中[python/paddle/fluid/layers/nn.py](https://github.com/PaddlePaddle/Paddle/blob/develop/python/paddle/fluid/layers/nn.py)的代码示例。
4. 单元测试
经过上述步骤之后,可以通过单元测试验证代码的正确性。通过比对C++计算结果和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.))
注意:
如果出现类似错误: 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。需要将relu2_op.so所在路径以及libpaddle_framework.so路径(即paddle.sysconfig.get_lib()得到路径)设置到环境变量LD_LIBRARY_PATH中, 对于Linux环境设置:
# 假如relu2_op.so,路径是: paddle/test
# 假如libpaddle_framework.so,路径是: pyenv/local/lib/python2.7/site-packages/paddle/libs
export LD_LIBRARY_PATH=paddle/test:pyenv/local/lib/python2.7/site-packages/paddle/libs:$LD_LIBRARY_PATH
在GitHub上贡献代码
飞桨非常欢迎大家在开源生态Github中贡献代码,与实时分享深度学习项目的成功应用和的奇思妙想。下面以PaddlePaddle/Paddle repo为例,详细介绍在GitHub上提交代码的操作方法,流程如 图1 所示。
图1 在GitHub上贡献代码流程
说明:
在执行如下操作前,请确保本地已经安装GIT,下载路径:https://git-scm.com/download/win,选择与PC系统对应的版本。
创建本地GitHub环境
说明:
如果首次使用飞桨GitHub,需要先创建飞桨本地GitHub环境。如果已经创建了飞桨本地GitHub,此步骤可忽略。
Fork仓库
登录飞桨GitHub首页,单击 Fork,生成自己目录下的仓库,如 https://github.com/USERNAME/Paddle。
Clone远程仓库到本地
任意选择一个本地目录,将远程仓库clone到本地,命令如下:
➜ git clone https://github.com/USERNAME/Paddle
➜ cd Paddle
创建本地分支
飞桨使用Git流分支模型进行开发、测试、发布和维护,特性开发和问题修复都要求在一个新的分支上完成。代码如下:
在 Paddle-develop 分支上,使用 git checkout -b 创建并切换到新分支。
➜ git checkout -b my-cool-stuff
说明:
在 checkout 之前,需要保持当前分支目录 clean,否则会把 untracked 的文件也带到新分支上,可以通过 git status 查看。
安装代码格式化插件
飞桨使用 pre-commit 管理 Git 预提交钩子,格式化源代码(C++,Python),在commit前自动检查代码基础质量的满足度(如每个文件只有一个 EOL,Git 中不允许添加大文件等)。
pre-commit测试是 Travis-CI 中单元测试的一部分,不满足钩子的 PR 不允许提交到飞桨。请在当前目录运行如下代码:
➜ pip install pre-commit
➜ pre-commit install
说明:
- 飞桨使用 clang-format 参数调整 C/C++ 源代码格式,请确保 clang-format 版本在 3.8 以上。
- 通过pip install pre-commit和conda install -c conda-forge pre-commit安装的yapf稍有不同。建议使用pip install pre-commit(飞桨开发人员使用的命令)。
更新本地仓库
说明:
如果是首次创建本地GitHub环境,代码已经和原仓库代码同步,此步骤可忽略。
同步原仓库代码如下:
- 通过 git remote 查看当前远程仓库的名字。
➜ git remote
origin
➜ git remote -v
origin https://github.com/USERNAME/Paddle (fetch)
origin https://github.com/USERNAME/Paddle (push)
这里 origin 是 clone 的远程仓库的名字,也就是自己用户名下的 Paddle。
- 创建一个原始 Paddle 仓库的远程主机,命名为 upstream。
➜ git remote add upstream https://github.com/PaddlePaddle/Paddle
➜ git remote
origin
upstream
- 获取 upstream 的最新代码并更新当前分支。
➜ git fetch upstream
➜ git pull upstream develop
开始开发
在本例中,删除了 README.md 中的一行,并创建了一个新文件。
通过 git status 查看当前状态,会提示当前目录的一些变化,同时也可以通过 git diff 查看文件具体被修改的内容。
➜ git status
On branch test
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
test21 ···
no changes added to commit (use "git add" and/or "git commit -a")
代码提交
commit代码到本地仓库
先取消对 README.md 文件的改变,然后提交新添加的 test 文件。
➜ git checkout -- README.md
➜ git status
On branch test
Untracked files:
(use "git add <file>..." to include in what will be committed)
test
nothing added to commit but untracked files present (use "git add" to track)
➜ git add test
Git 每次提交代码,都需要写提交说明,这可以让其他人知道这次提交做了哪些改变,这可以通过git commit 完成。
➜ git commit
CRLF end-lines remover...............................(no files to check)Skipped
yapf.................................................(no files to check)Skipped
Check for added large files..............................................Passed
Check for merge conflicts................................................Passed
Check for broken symlinks................................................Passed
Detect Private Key...................................(no files to check)Skipped
Fix End of Files.....................................(no files to check)Skipped
clang-formater.......................................(no files to check)Skipped
[my-cool-stuff c703c041] add test file
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 233
需要注意的是:需要在commit中添加说明(commit message)以触发CI单测,写法如下:
# 触发develop分支的CI单测
➜ git commit -m "test=develop"
# 触发release/1.1分支的CI单侧
➜ git commit -m "test=release/1.1"
Push代码到远程仓库
将本地的修改推送到 GitHub 上,也就是 https://github.com/USERNAME/Paddle。
# 推送到远程仓库 origin 的 my-cool-stuff 分支上
➜ git push origin my-cool-stuff
完成Pull Request
此时,可以去 https://github.com/USERNAME/Paddle 下查看,会发现my-cool-stuff 分支,切换到所建分支,单击 New pull request,如下图所示。
选择目标分支,如下图所示。
说明:
可以在PR描述中标识PR的功能,接下来等待 review。如果有需要修改的地方,参照上述步骤更新origin中的对应分支即可。
签署CLA
首次向飞桨GitHhub提交Pull Request时,需要签署CLA(Contributor License Agreement)协议,以保证的代码可以正常合入,操作方式如下:
- 查看PR中的 Check 部分,选择 license/cla ,单击detail,进入CLA网站,如下图所示。
点击CLA网站中的 Sign in with GitHub to agree ,完成后跳转到 Pull Request 页面,如下图所示。
CI测试
在Pull Request中每提交一次新的commit,都会触发CI单元测试,请确保commit message中已加入必要的修改说明。
注意:
Pull Request中的CI单元测试进程会持续几个小时,请及时关注。当所需的测试后都出现了绿色的对勾,表示本次的commit通过了CI单元测试。
后续处理
代码审查(Code Review)
提交PR后,开发人员会进行代码审查,如果提出修改意见,需要相应的进行确认或修改,再次提交代码。一旦code review通过,PR会被开发人员合入仓库。
删除远程分支
在 PR 被 merge 进主仓库后,可以在 PR 的页面删除远程仓库的分支。也可以使用 git push origin :分支名 删除远程分支,如:
➜ git push origin :my-cool-stuff
删除本地分支
最后,删除本地分支。
# 切换到 develop 分支
➜ git checkout develop
# 删除 my-cool-stuff 分支
➜ git branch -D my-cool-stuff