首页 > 数据库 > 数据库开发技术 > 如何在OneFlow中新增算子

本文将以开发一个 leaky_relu(准确说是 leaky_relu_yzh op,因为 master 分支的 leaky_relu 组合了其它知识点)为例介绍如何在 OneFlow 中新增算子(https://github.com/Oneflow-Inc/oneflow/pull/8350)。
1
背景
op 与 kernel 是两个有关联的概念。op 是逻辑上的算子,包含 OneFlow Compiler 在构建计算图时所需要的必要信息,如输入、输出形状,哪些张量需要自动求导等信息。有了 op 中的信息,OneFlow Compiler 就可以构建计算图并依据计算图做资源申请、构建等操作(如根据张量的输入输出大小申请内存), 但是 op 中不包含具体的处理数据的逻辑。
在真正需要处理数据时,OneFlow Runtime 会启动 kernel 完成计算,所以 kernel 中包含了具体处理数据的逻辑。对于一个逻辑上的 op,OneFlow Runtime 会根据数据类型、硬件设备(比如是 CPU 还是 CUDA)的具体情况,选择启动不同的 kernel。
OneFlow 中的系统 op 与 user op
def OneFlow_LeakyReluYZHOp : OneFlow_BaseOp<"leaky_relu_yzh", [NoSideEffect, DeclareOpInterfaceMethods<UserOpCompatibleInterface>]> {//...}
// 一个输入 xlet input = (insOneFlow_Tensor:$x);
// 两个输入:a, blet input = (insOneFlow_Tensor:$a,OneFlow_Tensor:$b);
let output = (outsOneFlow_Tensor:$out0,OneFlow_Tensor:$out1);
let attrs = (insDefaultValuedAttr<F32Attr, "0.">:$rate);
let attrs = (insF32Attr:$rate);
let has_check_fn = 1; // 生成属性检查接口let has_logical_tensor_desc_infer_fn = 1; // 生成 logical shape 推导接口let has_physical_tensor_desc_infer_fn = 1; // 生成 physical shape 推导接口let has_get_sbp_fn = 1; // 生成 get sbp 接口let has_sbp_signature_infer_fn = 1; // 生成 sbp signature 推导接口,未来会移除,推荐使用 has_nd_sbp_infer_fnlet has_data_type_infer_fn = 1; // 生成 data type 推导接口let has_device_and_stream_infer_fn = 1; // 生成 device 推导接口let has_input_arg_modify_fn = 1; // 生成输入 modify 接口,比如设置 is_mutable、requires_grad(用于Lazy)等let has_output_arg_modify_fn = 1; // 生成输出 modify 接口,比如设置 is_mutable、requires_grad(用于Lazy)等let has_output_blob_time_shape_infer_fn = 1; // 生成输出 time shape 推导接口let has_nd_sbp_infer_fn = 1; // 生成 nd sbp 推导接口
let has_logical_tensor_desc_infer_fn = 1;let has_physical_tensor_desc_infer_fn = 1;let has_data_type_infer_fn = 1;let has_get_sbp_fn = 1;
template<typename T>class CpuLeakyReluYZHKernel final : public user_op::OpKernel {public:CpuLeakyReluYZHKernel() = default;~CpuLeakyReluYZHKernel() = default;private:void Compute(user_op::KernelComputeContext* ctx) const override {const user_op::Tensor* x = ctx->Tensor4ArgNameAndIndex("x", 0);user_op::Tensor* y = ctx->Tensor4ArgNameAndIndex("y", 0);const int32_t elem_cnt = x->shape().elem_cnt();const T* x_ptr = x->dptr<T>();T* y_ptr = y->mut_dptr<T>();const auto alpha = ctx->Attr<float>("alpha");FOR_RANGE(int32_t, i, 0, elem_cnt) { y_ptr[i] = x_ptr[i] > 0 ? x_ptr[i] : alpha * x_ptr[i]; }}bool AlwaysComputeWhenAllOutputsEmpty() const override { return false; }};
#define REGISTER_CPU_LEAKY_RELU_YZH_KERNEL(dtype) \REGISTER_USER_KERNEL("leaky_relu_yzh") \.SetCreateFn<CpuLeakyReluYZHKernel<dtype() \.SetIsMatchedHob((user_op::HobDeviceType() == DeviceType::kCPU) \&& (user_op::HobDataType("y", 0) == GetDataType<dtype>::value));
template<typename T>__global__ void LeakyReluForwardGpu(const int n, const float alpha, const T* x, T* y) {CUDA_1D_KERNEL_LOOP(i, n) { y[i] = x[i] > 0 ? x[i] : x[i] * alpha; }}
#define RUN_CUDA_KERNEL(func, device_ctx_ptr, thread_num, ...) \func<<<SMBlocksNum4ThreadsNum(thread_num), kCudaThreadsNumPerBlock, 0, \(device_ctx_ptr)->cuda_stream()>>>(__VA_ARGS__)
可以看到不同设备类的 Compute 中大部分代码是重复的。一种更优的代码组织方式是用一个 .cpp 文件完成 kernel 和注册的逻辑,.cu 文件编写 GPU Kernel 函数和 GPU 模板特化的代码,.h 文件用于定义和编写注册宏。可参考 dim_gather_kernel_* (https://github.com/Oneflow-Inc/oneflow/tree/master/oneflow/user/kernels)中的代码。
OneFlow 为了适配多种设备,还提供了 Primitive 组件,可以参考:Primitive PR(https://github.com/Oneflow-Inc/oneflow/pull/6234)
┌─────────────┐│ Module ││ (Python) │├─────────────┤│ ││ Functional │├─────────────┤│ ││ Op/Kernels ││ (C++) │└─────────────┘
class LeakyReluYZHFunctor {public:LeakyReluYZHFunctor() {op_ = CHECK_JUST(one::OpBuilder("leaky_relu_yzh").Input("x").Output("y").Build());}Maybe<Tensor> operator()(const std::shared_ptr<one::Tensor>& x, const float& alpha) const {MutableAttrMap attrs;JUST(attrs.SetAttr<float>("alpha", alpha));return OpInterpUtil::Dispatch<one::Tensor>(*op_, {x}, attrs);}private:std::shared_ptr<OpExpr> op_;};
m.add_functor<impl::LeakyReluYZHFunctor>("LeakyReluYZH"); // 注意最后字符串中的名字在后续的 functional_api.yaml 中会用到
- name: "leaky_relu_yzh"signature: "Tensor (Tensor x, Float alpha) => LeakyReluYZH"bind_python: True
flow._C.leaky_relu_yzh(...)import oneflow as flowimport numpy as npx_tensor = flow.Tensor(np.random.randn(3, 3))out = flow._C.leaky_relu_yzh(x_tensor, alpha=0.2)
- name: "leaky_relu_yzh_grad"signature: "Tensor (Tensor x, Tensor dy, Float alpha) => LeakyReluYZHGrad"bind_python: False实现用于求导的反向逻辑
struct LeakyReluYZHCaptureState : public AutoGradCaptureState {bool requires_grad; // 输入x是否需要梯度float alpha=0.0; // 输入的参数alpha};
class LeakyReluYZH : public OpExprGradFunction<LeakyReluYZHCaptureState> {public:Maybe<void> Init(const OpExpr& op) override {//...}Maybe<void> Capture(LeakyReluYZHCaptureState* ctx, const TensorTuple& inputs,const TensorTuple& outputs, const AttrMap& attrs) const override {//...}Maybe<void> Apply(const LeakyReluYZHCaptureState* ctx, const TensorTuple& out_grads,TensorTuple* in_grads) const override {//...}};
Maybe<void> Init(const OpExpr& op) override {const auto* fw_op_expr = dynamic_cast<const UserOpExpr*>(&op);CHECK_NOTNULL_OR_RETURN(fw_op_expr);base_attrs_ = MakeAttrMapFromUserOpConf(fw_op_expr->proto());return Maybe<void>::Ok();}
Maybe<void> Capture(LeakyReluYZHCaptureState* ctx, const TensorTuple& inputs,const TensorTuple& outputs, const AttrMap& attrs) const override {CHECK_EQ_OR_RETURN(inputs.size(), 1); // 判断输入个数是否为1ctx->requires_grad = inputs.at(0)->requires_grad(); // 判断输入是否需要梯度if (!ctx->requires_grad) { return Maybe<void>::Ok(); } // 如果不需要梯度,也就不需要求导了,直接返回 Maybe<void>::Ok()ComposedAttrMap composed_attrs(attrs, base_attrs_);ctx->alpha = JUST(composed_attrs.GetAttr<float>("alpha")); // 获取 alpha,并存入 ctx->alpha 中ctx->SaveTensorForBackward(inputs.at(0)); // 调用 SaveTensorForBackward 方法,保存输入的 Tensorreturn Maybe<void>::Ok();}
Maybe<void> Apply(const LeakyReluYZHCaptureState* ctx, const TensorTuple& out_grads,TensorTuple* in_grads) const override {CHECK_EQ_OR_RETURN(out_grads.size(), 1); // 检查梯度 Tensor 个数是否为 1in_grads->resize(1); // 因为输入只有一个,所以我们 resize(1)if (ctx->requires_grad) {const auto& x = ctx->SavedTensors().at(0); // 调用 SavedTensors 接口,拿到先前通过 SaveTensorForBackward 接口保存的 Tensorin_grads->at(0) = JUST(functional::LeakyReluYZHGrad(x, out_grads.at(0), ctx->alpha)); // 拿到 x,dy,alpha,传入给 LeakyReluYZHGrad 计算,并将梯度返回给 in_grads->at(0)}return Maybe<void>::Ok();}
REGISTER_OP_EXPR_GRAD_FUNCTION("leaky_relu_yzh", LeakyReluYZH); // 第二个参数是用于求导的类名为 Graph 模式注册反向
REGISTER_USER_OP_GRAD("leaky_relu_yzh").SetBackwardOpConfGenFn([](user_op::BackwardOpConfContext* ctx) -> Maybe<void> {// 根据前向的 op type name,拼凑出一个 leaky_relu_yzh_grad_op_name (leaky_relu_yzh_grad)const std::string leaky_relu_yzh_grad_op_name = ctx->FwOp().op_name() + "_grad";ctx->DefineOp(leaky_relu_yzh_grad_op_name, [&ctx](user_op::BackwardOpBuilder& builder) {// 构建一个 op(op type name 为 leaky_relu_yzh_grad)// 把前向输出 y 的梯度,作为 leaky_relu_yzh_grad 的输入 dy// 把前向的 x 作为 leaky_relu_yzh_grad 的输入 x// 输出为 dx// attr alpha 同前向一样return builder.OpTypeName("leaky_relu_yzh_grad").InputBind("dy", ctx->FwOp().output_grad("y", 0)).InputBind("x", ctx->FwOp().input("x", 0)).Attr("alpha", ctx->FwOp().attr<float>("alpha")).Output("dx").Build();});// 把 leaky_relu_yzh_grad_op_name 算子的输出 dx 的结果// 绑定到前向输入 x 的反向梯度上// 即:// leaky_relu_yzh 的输入 x 的梯度 = leaky_relu_yzh_grad 的输出 dxctx->FwOp().InputGradBind(user_op::OpArg("x", 0),[&ctx, &leaky_relu_yzh_grad_op_name]() -> const std::string& {return ctx->GetOp(leaky_relu_yzh_grad_op_name).output("dx", 0);});return Maybe<void>::Ok();});3 测试与文档
原文作者:OneFlow;如有侵权,请联系管理员删除。