• Autograd解析|OneFlow学习笔记


    cb4653cc629488019372ef826c85719d.png

    撰文|月踏

    更新|赵露阳

    前文《AI杂谈:手推BP》讲了Backward Propagation的数学原理。本文以OneFlow的代码为例,梳理Autograd模块的实现细节。

    1

    一个求梯度的小例子

    先看下面这个简单的例子:

    1. import oneflow as of
    2. x = of.randn(2, 2, requires_grad=True)
    3. y = x + 100
    4. z = y.sum()
    5. z.backward()

    forward pass可以对应到下面的计算图:

    0c04dc4510adfa7a3c80be9679a18bcc.png

     图1

    即对应下面公式:

    894f89f3d5310e804e3730e03815b681.png

    根据前文《AI杂谈:手推‍BP》很容易手动计算出x的梯度值,即: 

    4d91124a0d1ce72ccd7153d0ee971275.png

    x1、x2、x3的计算过程类似,不再赘述,下面看一下OneFlow的执行结果,执行print(x.grad)可得到如下输出:

    1. tensor([[1., 1.],
    2. [1., 1.]], dtype=oneflow.float32)

    可以看出,结果和前面公式(3)的计算结果一致,下面通过具体的代码实现来分析OneFlow的Autograd模块。

    2

    backward接口

    上面例子中的python端的backward接口,调用的是python/oneflow/framework/tensor.py中的_backward接口:

    1. def _backward(self, gradient=None, retain_graph=False, create_graph=False):
    2. if not lazy_mode.is_enabled():
    3. flow.autograd.backward(self, gradient, retain_graph, create_graph)
    4. else:
    5. ...

    可以看到backward只支持eager模式,这是因为graph静态图模式下,计算图是提前编译好的,无需手动通过.backward()调用。flow.autograd.backward()会调用oneflow/api/python/autograd/autograd.cpp中导出的backward方法:

    1. ONEFLOW_API_PYBIND11_MODULE("autograd", m) {
    2. m.def("backward", &Backward);
    3. m.def("grad", &Grad);
    4. }

    从pybind定义来看,这里面总共导出了两个接口(autograd.backward和autograd.grad)。其中,backward是对所有的requires_grad属性为True的节点求梯度,grad只对指定的叶子结点求梯度,原理上是相同的,本文只以backward为例来看代码的实现,backward接口会调用到同一个文件中的Backward函数:

    1. Maybe<one::TensorTuple> Backward(const one::TensorTuple& outputs, const one::TensorTuple& out_grads,
    2. bool retain_graph, bool create_graph) {
    3. if (create_graph) { retain_graph = true; }
    4. std::shared_ptr<one::TensorTuple> gradients = JUST(CheckAndInitOutGrads(outputs, out_grads));
    5. JUST(one::GetThreadLocalAutogradEngine()->RunBackwardAndSaveGrads4LeafTensorIf(
    6. outputs, *gradients, retain_graph, create_graph));
    7. return std::make_shared<one::TensorTuple>(0);
    8. }

    这里的GetThreadLocalAutogradEngine()可以看作是一个thread_local的单例,位于oneflow/core/autograd/autograd_engine.cpp,返回一个autograd引擎(AutogradEngine)对象的指针:

    1. AutogradEngine* GetThreadLocalAutogradEngine() {
    2. thread_local static GraphAutogradEngine autograd_engine;
    3. return &autograd_engine;
    4. }

    AutogradEngine是OneFlow的Autograd的核心数据结构,它的继承关系如下:

    b677d3671692e36eb133e47e0e053224.png

    图2

    这里autograd引擎的子类实现有基于栈式的、基于图式的实现,默认使用基于图式的GraphAutogradEngine。从前面代码中可以看到,获取autograd引擎指针后,通过调用RunBackwardAndSaveGrads4LeafTensor函数,位于oneflow/core/autograd/autograd_engine.cpp:L315

    1. Maybe<void> GraphAutogradEngine::RunBackwardAndSaveGrads4LeafTensor(const TensorTuple& outputs,
    2. const TensorTuple& out_grads,
    3. bool retain_graph,
    4. bool create_graph) {
    5. for (int i = 0; i < outputs.size(); ++i) {
    6. JUST(JUST(outputs.at(i)->current_grad())->PushPartialTensor(out_grads.at(i)));
    7. }
    8. GraphTask graph_task(outputs, retain_graph, create_graph);
    9. JUST(graph_task.ComputeDependencies());
    10. JUST(graph_task.Apply(/*save_grad_for_leaf=*/true));
    11. return Maybe<void>::Ok();
    12. }

    这就真正进入了autograd模块的内部处理流程,后面继续分析。

    3

    FunctionNode和建立反向图

    在进行backward pass时,执行的是一张反向图,反向图中的节点是在forward pass的时候建立的,其中的每个节点被称作FunctionNode,主要数据结构如下:

    0c47c3c3d20f32ebaf397170c208ac4b.png

    图3

    先说图3中FunctionNode(oneflow/core/autograd/autograd_engine.h:L42),包含next_functions_、input_meta_data_、output_meta_data_这三个数据成员,其中next_functions_表示出边,另外两个表示一些meta信息,下面列几个主要的:

    • is_leaf_:是不是叶子节点

    • requires_grad_:是不是需要求梯度值

    • retain_grad_:对于非叶子节点,是不是保存梯度值

    • acc_grad_:在gradient accumulation的的情况下,多个mini-batch的梯度累加

    • current_grad_:当前这个batch的梯度值

    我们用到的是GraphFunctionNod(oneflow/core/autograd/autograd_engine.cpp:L178)

    1. GraphFunctionNode::GraphFunctionNode(const std::string& name,
    2. const std::shared_ptr<BackwardFunction>& backward_fn,
    3. const TensorTuple& inputs, const TensorTuple& outputs)
    4. : FunctionNode(name, backward_fn) {
    5. input_meta_data_.resize(inputs.size());
    6. next_functions_.reserve(inputs.size());
    7. for (int i = 0; i < inputs.size(); ++i) {
    8. if (inputs.at(i)->requires_grad()) {
    9. input_meta_data_.at(i) = inputs.at(i)->mut_autograd_meta();
    10. next_functions_.emplace_back(inputs.at(i)->mut_grad_fn_node());
    11. }
    12. }
    13. output_meta_data_.resize(outputs.size());
    14. output_tensor_infos_.reserve(outputs.size());
    15. for (int i = 0; i < outputs.size(); ++i) {
    16. const auto& autograd_meta =
    17. NewAutogradMeta(outputs.at(i)->requires_grad(), outputs.at(i)->is_leaf());
    18. outputs.at(i)->set_autograd_meta(autograd_meta);
    19. output_meta_data_.at(i) = outputs.at(i)->mut_autograd_meta();
    20. output_tensor_infos_.emplace_back(TensorInfo(*outputs.at(i)));
    21. }
    22. backward_fn_ = backward_fn;
    23. }

    可见它主要对FunctionNode中的重要数据成员做了初始化,其中input_meta_data_、output_meta_data_中的AutogradMeta信息是从相应的input、output tensor中获取的,tensor通过桥接模式保存了一个TensorImpl对象指针,这个TensorImpl对象则维护了一个AutogradMeta对象。

    继续看下FunctionNode中的反向函数backward_fn_,在《OneFlow学习笔记:从Functor到OpExprInterpreter》中讲到了在进行一个op调用的时候会执行AutogradInterpreter::Apply这个函数(oneflow/core/framework/op_interpreter/op_interpreter.cpp:L86),里面会创建这个反向函数:

    1. Maybe<void> AutogradInterpreter::Apply(
    2. const OpExpr& op_expr,
    3. const TensorTuple& inputs,
    4. TensorTuple* outputs,
    5. const OpExprInterpContext& ctx) const {
    6. ...
    7. autograd::AutoGradMode mode(false);
    8. JUST(internal_->Apply(op_expr, inputs, outputs, ctx));
    9. std::shared_ptr<OpExprGradClosure> grad_closure(nullptr);
    10. if (requires_grad && !LazyMode::is_enabled()) {
    11. grad_closure = JUST(op_expr.GetOrCreateOpGradClosure());
    12. auto backward_fn = std::make_shared<BackwardFunction>();
    13. backward_fn->body = [=](const TensorTuple& out_grads, TensorTuple* in_grads,
    14. bool create_graph) -> Maybe<void> {
    15. autograd::AutoGradMode mode(create_graph);
    16. JUST(grad_closure->Apply(out_grads, in_grads));
    17. return Maybe<void>::Ok();
    18. };
    19. backward_fn->status = [=]() { return grad_closure->state()->SavedTensors().size() > 0; };
    20. JUST(GetThreadLocalAutogradEngine()->AddNode(op_expr.op_type_name() + "_backward", backward_fn,
    21. inputs, outputs));
    22. }
    23. ...
    24. return Maybe<void>::Ok();
    25. }

    可以看到反向图节点的名字是以正向图op的type name加上_backward的后缀来组成的,使用AddNode方法来创建FunctionNode(oneflow/core/autograd/autograd_engine.cpp:L356

    1. Maybe<FunctionNode> GraphAutogradEngine::AddNode(
    2. const std::string& name, const std::shared_ptr<BackwardFunction>& backward_fn,
    3. const TensorTuple& inputs, TensorTuple* outputs) {
    4. // Firstly push function_node of tensor in stack which is leaf and requires_grad
    5. for (const std::shared_ptr<Tensor>& in_tensor : inputs) {
    6. if (in_tensor->is_leaf() && in_tensor->requires_grad()) {
    7. if (!in_tensor->grad_fn_node()) { JUST(AddAccumulateFunctionNode(in_tensor)); }
    8. }
    9. }
    10. std::shared_ptr<FunctionNode> func_node =
    11. std::make_shared<GraphFunctionNode>(name, backward_fn, inputs, *outputs);
    12. for (const std::shared_ptr<Tensor>& out_tensor : *outputs) {
    13. out_tensor->set_grad_fn_node(func_node);
    14. }
    15. return func_node;
    16. }

    可见FunctionNode是挂在Tensor上的,通过Tensor的set_grad_fn_node接口维护到Tensor的数据结构中,在《OneFlow学习笔记:Global View的相关概念和实现》中画过Tensor的继承关系图,FunctionNode就是保存在TensorIf中:

    1f2b936a6fb8e4fd31e7e80f28e5f669.png

    图4

    至此,已经理清了FunctionNode中各个成员的作用以及来历,假如以第二节的图1为例来画出对应的反向图的话,如下图所示:

    4e5d681012223addf214b8c1a0386e5a.png

    图5

    计算好的梯度值会被放到output_meta_data_中得AutogradMeta中,它可以通过tensor的acc_grad、current_grad接口来获取。

    4

    反向图的执行流程

    接第三节列出的最后一段代码,其中最重要的两句话是:

    1. ...
    2. JUST(graph_task.ComputeDependencies());
    3. JUST(graph_task.Apply(/*save_grad_for_leaf=*/true));
    4. ...

    这里面的graph_task是GraphTask类型,它是一个很重要的数据结构,用来调度反向图中所有FunctionNode的执行,下面列一下它的主要成员:

    1. class GraphTask final {
    2. bool retain_graph_;
    3. bool create_graph_;
    4. std::vector<FunctionNode*> roots_;
    5. HashMap<FunctionNode*, int> dependencies_;
    6. HashSet<FunctionNode*> need_execute_;
    7. };

    先看本节开头的graph_task.ComputeDependencies,它主要是在初始化dependencies_这个map,这个map维护了每个FunctionNode的入度信息,再看graph_task.Apply,它主要是在通过拓扑序来访问反向图中的每个FunctionNode,并且对当前的FunctionNode进行各种操作(oneflow/core/autograd/autograd_engine.cpp:L287

    1. Maybe<voidGraphTask::Apply(bool save_grad_for_leaf) {
    2. std::queue<FunctionNode*> queue;
    3. for (FunctionNode* node : roots_) {
    4. if (dependencies_[node] == 0) { queue.push(node); }
    5. }
    6. while (!queue.empty()) {
    7. FunctionNode* node = queue.front();
    8. queue.pop();
    9. if (!need_execute_.empty() && need_execute_.find(node) == need_execute_.end()) {
    10. node->ReleaseOutTensorArgs();
    11. continue;
    12. }
    13. if (/*bool not_ready_to_apply=*/!(JUST(node->Apply(create_graph_)))) { continue; }
    14. if (save_grad_for_leaf) { JUST(node->AccGrad4LeafTensor(create_graph_)); }
    15. JUST(node->AccGrad4RetainGradTensor());
    16. node->ReleaseOutTensorArgs();
    17. if (!retain_graph_) { node->ReleaseData(); }
    18. for (const auto& next_grad_fn : node->next_functions()) {
    19. FunctionNode* next_node = next_grad_fn.get();
    20. dependencies_[next_node] -= 1;
    21. if (dependencies_[next_node] == 0) { queue.push(next_node); }
    22. }
    23. }
    24. return Maybe<void>::Ok();
    25. }

    这里最重要的是下面两个语句:

    1. node->Apply

    2. node->AccGrad4LeafTensor

    下面来逐个分析,先看node->Apply(oneflow/core/autograd/autograd_engine.cpp:L143),首先利用output_meta_data_初始化了output_grads,把它作为反向函数的输入,调用反向函数来求梯度值,求出的梯度值暂存在input_grads中,然后再更新到input_meta_data_中:

    1. Maybe<bool> FunctionNode::Apply(bool create_graph) {
    2. ...
    3. JUST(backward_fn_->body(output_grads, &input_grads, create_graph));
    4. for (int i = 0; i < input_meta_data_.size(); ++i) {
    5. if (input_grads.at(i)) {
    6. ...
    7. JUST(input_meta_data_.at(i)->current_grad()->PushPartialTensor(input_grads.at(i)));
    8. }
    9. }
    10. return true;
    11. }

    再看node->AccGrad4LeafTensor,这个函数最终会调用到CopyOrAccGrad,它主要用于在gradient accumulation的时候,多个mini-batch之间把梯度值多累加,和如果有hook函数的的话,使用注册的hook对当前的梯度值进行处理:

    1. Maybe<void> CopyOrAccGrad(AutogradMeta* autograd_meta, bool autograd_mode) {
    2. autograd::AutoGradMode mode(autograd_mode);
    3. auto current_grad = JUST(autograd_meta->current_grad()->GetAccTensor({}));
    4. if (!current_grad) { return Maybe<void>::Ok(); }
    5. if (autograd_meta->acc_grad()) {
    6. ...
    7. DevVmDepObjectConsumeModeGuard guard(DevVmDepObjectConsumeMode::NONE);
    8. const auto& output = JUST(functional::Add(autograd_meta->acc_grad(), current_grad, /*alpha=*/1,
    9. /*inplace=*/autograd_meta->is_grad_acc_inplace()));
    10. JUST(autograd_meta->set_acc_grad(output));
    11. } else {
    12. JUST(autograd_meta->set_acc_grad(current_grad));
    13. }
    14. for (const auto& hook : autograd_meta->post_grad_accumulation_hooks()) {
    15. auto new_grad = hook(autograd_meta->acc_grad());
    16. if (new_grad) { JUST(autograd_meta->set_acc_grad(new_grad)); }
    17. }
    18. return Maybe<void>::Ok();
    19. }

    (特别感谢同事yinggang中间的各种答疑解惑。本文主要参考代码:https://github.com/Oneflow-Inc/oneflow/commit/a4144f9ecb7e85ad073a810c3359bce7bfeb05e1)

    其他人都在看

    欢迎下载体验OneFlow v0.7.0:GitHub - Oneflow-Inc/oneflow: OneFlow is a performance-centered and open-source deep learning framework.OneFlow is a performance-centered and open-source deep learning framework. - GitHub - Oneflow-Inc/oneflow: OneFlow is a performance-centered and open-source deep learning framework.https://github.com/Oneflow-Inc/oneflow

  • 相关阅读:
    Euro-NCAP-HWA测试流程中文版V1.1(2023发布)
    【LeetCode-简单】746. 使用最小花费爬楼梯(详解)
    关于学生使用flowus的一些事
    Android一个有用又有趣的知识点:BindingAdapter
    面试题 02.03. 删除中间节点
    java用双大括构造方式进行号初始化赋值操作
    使用sql判断两段时间是否重叠
    JVM高频知识合集(面试)【1】
    仿哔哩哔哩微信小程序源码
    python开发基础篇1——后端操作K8s API方式
  • 原文地址:https://blog.csdn.net/OneFlow_Official/article/details/124763080