其他
飞桨框架v2.3发布高可复用算子库PHI!重构开发范式,降本增效
2022年5月,飞桨框架2.3版本正式发布,设计实现了高可复用算子库PHI(Paddle High reusability operator library)。新算子库提供了百余个与Python开发接口保持一致的C++运算类API,可大幅降低框架原生算子和自定义算子的开发成本。
算子库在框架中扮演的角色
PHI架构简介
基于PHI的低成本算子开发
配置式算子定义:采用基于 yaml 配置的算子定义方式,替换掉了原先基于 C++ 类的算子定义方式,写法上更加简洁清晰,同时解耦框架实现也提升了灵活性。 函数式算子内核:支持通过调用函数的方式复用基础算子内核来组合开发更复杂的算子内核。例如,通过对矩阵乘、加减乘除法等基础算子函数式接口的调用,很容易实现一个 Linear 或者 SGD 算子内核,高效支撑算子开发需求。以复杂的 Einsum 算子为例,基于 PHI 算子库进行组合式实现,所需代码量相比从零开始实现能够显著减少,开发成本显著降低。 基于 C++运算 API 的插件式算子开发:我们将之前发布的自定义算子机制与 PHI 算子库进行了整合,支持在开发自定义算子时使用 PHI 提供的 C++ 运算 API,有效降低了外部算子的开发成本。
其次,在降低新硬件接入成本方面,目前主要有两套机制:
插件式算子内核开发:我们支持在外部实现特定硬件的算子内核,与框架实现解耦,以插件的形式接入框架使用,支持低成本接入硬件或相应加速库。 Primitive 内核接口体系:支持不同芯片之间的算子内核复用代码实现,提升算子内核的开发效率。例如,基于 Primitive 接口的算子内核的复用,能够使用同一份代码支持 GPU 和 XPU 硬件,有效降低了新硬件适配算子的成本。
配置式算子定义
class TransposeOp : public framework::OperatorWithKernel {
public:
using framework::OperatorWithKernel::OperatorWithKernel;
void InferShape(framework::InferShapeContext *ctx) const override {...}
};
class TransposeOpMaker : public framework::OpProtoAndCheckerMaker {
public:
void Make() override {
AddInput(
"X",
"(Tensor) The input tensor, tensors with rank up to 6 are supported.");
AddOutput("Out", "(Tensor)The output tensor.");
AddAttr<std::vector<int>>(
"axis",
"(vector<int>) A list of values, and the size of the list should be "
"the same with the input tensor rank. This operator permutes the input "
"tensor's axes according to the values given.");
AddComment(R"DOC(Transpose Operator.)DOC");
}
};
// PHI算子定义方式
- api : transpose
args : (Tensor x, int[] axis)
output : Tensor
infer_meta :
func : TransposeInferMeta
kernel :
func : transpose
backward : transpose_grad
args : (Tensor x)
output : Tensor
infer_meta :
func : UnchangedInferMeta
kernel :
func : conj
backward : conj_grad
- api : atan
args : (Tensor x)
output : Tensor
infer_meta :
func : UnchangedInferMeta
kernel :
func : atan
backward : atan_grad
args : (Tensor x, DataType dtype=DataType::UNDEFINED, Place place = {})
output : Tensor
invoke : full_like(x, 0, dtype, place)
函数式算子内核
仿函数类 +Context 输入形式内核:
class TransposeKernel : public framework::OpKernel<T> {
public:
void Compute(const framework::ExecutionContext& context) const override {
auto* x = context.InputVar("X");
auto* out = context.OutputVar("Out");
std::vector<int> axis = context.Attr<std::vector<int>>("axis");
// Kernel Implementation
}
};
函数式内核
void TransposeKernel(const Context& ctx,
const DenseTensor& x,
const std::vector<int>& axis,
DenseTensor* out) {
// Kernel Implementation
}
template <typename T, typename Context>
void SquareKernel(const Context& dev_ctx,
const DenseTensor& x,
DenseTensor* out) {
PowKernel<T>(dev_ctx, x, 2, out);
}
PD_REGISTER_KERNEL(
square, CPU, ALL_LAYOUT, phi::SquareKernel, float, double, int, int64_t) {}
// square反向内核实现及注册
template <typename T, typename Context>
void SquareGradKernel(const Context& dev_ctx,
const DenseTensor& x,
const DenseTensor& out_grad,
DenseTensor* x_grad) {
MultiplyKernel<T>(dev_ctx,
out_grad,
Multiply<T>(dev_ctx, Full<T>(dev_ctx, {1}, 2.0), x),
x_grad);
}
PD_REGISTER_KERNEL(square_grad,
CPU, ALL_LAYOUT, phi::SquareGradKernel, float, double, int, int64_t) {}
A() {
判断满足CPU执行条件:// 运行时判断
全局映射表中查找是否存在A_CPU()内核;// 运行时查找
找到A_CPU()并调用
判断满足GPU执行条件:
全局映射表中查找是否存在A_CPU()内核;
找到A_CPU()并调用
}
// 静态内核分发
// 编译时生成了A<CPU>和A<GPU>的函数内核,通过实际Device模板参数匹配所需内核,无需运行时判断查找
A<Device>()
基于 C++ 运算 API 的插件式算子开发
PADDLE_API Tensor acos(const Tensor& x);
PADDLE_API Tensor acosh(const Tensor& x);
PADDLE_API Tensor add(const Tensor& x, const Tensor& y);
...
const paddle::Tensor& weight,
const paddle::Tensor& bias) {
return {paddle::add(paddle::matmul(x, weight), bias)};
}
先计算 x_grad,对于 x_grad 的计算逻辑相对比较简单,只需要将 out_grad 和 weight 需只要做简单的 matmul 乘法,在乘的时候需要把 weight 作为一个 transpose 即可以得到一个 x_grad 的结果。 对于 weight_grad 的逻辑稍微有些复杂,其中会遇到一个高维的 shape,比如说 x.shape 可能是三维或者四维。在这种情况下,我们需要将 x.shape、out_grad.shape 做一个 reshape 的操作,把它变成一个二维的 Tensor 之后再进行matmul 的乘法。在这次乘的时候需要将 x reshape 之后的结果做 transpose。基于这种写法,可以处理大于二维的情况。 对于 bias_grad 的计算逻辑,因为 bias 是个一维的 Tensor,在前向运算的时候是做了一个映射的 broadcast,反向的时候要做一个类似 reduce 的操作。reduce 的逻辑,是需要动态地运算它要 reduce 的维度,这里边可以通过一个for循环将它 reduce 的维度计算出来,就是 rank-1个axis,之后调用 sum 运算,就可以得到 bias_grad。
const paddle::Tensor& weight,
const paddle::Tensor& bias,
const paddle::Tensor& out_grad) {
auto x_grad = paddle::matmul(out_grad, weight, false, true);
auto flatten_x = paddle::reshape(x, {-1, weight.shape()[0]});
auto flatten_out_grad = paddle::reshape(out_grad, {-1, weight.shape()[1]});
auto weight_grad = paddle::matmul(flatten_x, flatten_out_grad, true, false);
std::vector<int64_t> axis;
for (size_t i = 0; i+1 < x.shape().size(); ++i) {
axis.emplace_back(i);
}
auto bias_grad = paddle::experimental::sum(out_grad, axis);
return {x_grad, weight_grad, bias_grad};
}
结语
参考资料
[1]https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/transpose_cn.html
[2]https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/zeros_like_cn.html
[3]https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/full_like_cn.html
[4]https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/custom_op/new_cpp_op_cn.html#pythonc-api
[5]https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/nn/Linear_cn.html