[Анализ исходного кода] Как Pytorch реализует обратное распространение (1) ---- вызов движка

глубокое обучение PyTorch

0x00 сводка

В этой серии будет проанализировано, как реализована функция автоматической дифференциации PyTorch, примерно в десяти статьях. Эта статья — первая часть обратного распространения, представляющая процесс вызова: как войти в механизм автоградации C++ из кода Python.

Первые несколько статей в серии связаны ниже:

Автоматическая дифференциация инструментов глубокого обучения (1)

Автоматическая дифференциация инструментов глубокого обучения (2)

Автоматическая дифференциация оружия глубокого обучения (3) --- Пример интерпретации

[Анализ исходного кода] Как PyTorch реализует прямое распространение (1) --- Базовый класс (1)

[Анализ исходного кода] Как PyTorch реализует прямое распространение (2) --- Базовый класс (ниже)

[Анализ исходного кода] Как PyTorch реализует прямое распространение (3) --- конкретная реализация

0x01 предыдущий обзор

Сначала мы рассмотрим связь между прямым распространением и обратным распространением с трех точек зрения.

1.1 Тренировочный процесс

Давайте сначала вспомним тренировочный процесс.

Нейронная сеть (NN) представляет собой набор вложенных функций, выполняемых над некоторыми входными данными. Эти функциипараметр(состоит из весов и смещений), этипараметрХранится в тензорах в PyTorch. Обучение NN выполняется в два этапа:

  • прямое распространение: при прямом проходе нейронная сеть делает наилучшее предположение о правильном выходе. Он делает это предположение, пропуская входные данные через каждую из своих функций.
  • обратное распространение: при обратном распространении нейронная сеть регулирует свои параметры пропорционально ошибке в своем предположении. Он работает путем обхода вывода в обратном направлении, собирая информацию об аргументах функции (градиент) и оптимизировать параметры, используя для этого градиентный спуск.

1.2 Примеры

Во-вторых, давайте вспомним предыдущий пример.

        def train_loop(model, optimizer, iterations):
            for _ in range(iterations):
                optimizer.zero_grad()
                output = model(input) # 前向传播
                loss = criterion(output, target) # 计算损失
                loss.backward() # 反向传播
                optimizer.step()

После завершения прямого расчета мы получили зависимости графа расчета, поэтому можем приступить к обратному распространению. Нам нужно начать анализ с обратной стороны.

1.3 Анализ исходного кода

Как видно из предыдущей статьи, функция прямого расчета sub_Tensor имеет следующую конфигурацию для результата прямого расчета:

  • Как узнать, как вызвать обратный расчет: результат является результатом прямого вычисления, и естьautograd_meta_, который является типом DifferentiableViewMeta, а grad_fn_ для DifferentiableViewMeta — это функция градиента, вычисляемая в обратном порядке.grad_fn_ указывает на SubBackward0.
  • Как рассчитывается обратное распространение: вызвать вычисление SubBackward0.
  • Вход для SubBackward0: получен выходной результат прямого вычисления (он будет использоваться как входная переменная при обратном распространении, то есть он установлен в значение SubBackward0.input_metadata_ Above).
  • Выход SubBackward0:Построеноnext_edges_как выходное ребро во время его обратного распространения. в соответствии сnext_edges_Можно получить обратную диаграмму проводимости.

Теперь, когда мы разобрались в связи между прямым распространением и обратным распространением, давайте посмотрим, как войти в канал обратного распространения.

0x02 Процесс вызова Python

2.1 вызов

Мы впервые пришли к torch/_tensor.py, там есть две функции для расчета градиента, мы выбираем назад, чтобы увидеть.

def backward(self, gradient=None, retain_graph=None, create_graph=False, inputs=None):
    r"""Computes the gradient of current tensor w.r.t. graph leaves.
    """
    if has_torch_function_unary(self):
        return handle_torch_function(
            Tensor.backward,
            (self,),
            self,
            gradient=gradient,
            retain_graph=retain_graph,
            create_graph=create_graph,
            inputs=inputs)
    torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)

потом пришелtorch/autograd/__init__.py. Основная логика обратного здесь такова:

  • Используйте входные параметры для построения входных тензоров и тензоров градиента.
  • Используйте _make_grads, чтобы реорганизовать элементы в grad_tensors в форму кортежа (список (torch.Tensor, ...)) .
  • Затем используйте Variable._execution_engine.run_backward для выполнения обратного распространения.
def backward(
    tensors: _TensorOrTensors,
    grad_tensors: Optional[_TensorOrTensors] = None,
    retain_graph: Optional[bool] = None,
    create_graph: bool = False,
    grad_variables: Optional[_TensorOrTensors] = None,
    inputs: Optional[_TensorOrTensors] = None,
) -> None:
    r"""Computes the sum of gradients of given tensors with respect to graph
    leaves.
    """
    if grad_variables is not None:
        warnings.warn("'grad_variables' is deprecated. Use 'grad_tensors' instead.")
        if grad_tensors is None:
            grad_tensors = grad_variables
        else:
            raise RuntimeError("'grad_tensors' and 'grad_variables' (deprecated) "
                               "arguments both passed to backward(). Please only "
                               "use 'grad_tensors'.")
    if inputs is not None and len(inputs) == 0:
        raise RuntimeError("'inputs' argument to backward() cannot be empty.")
​
    # 利用输入参数来构建输入张量和梯度张量    
    tensors = (tensors,) if isinstance(tensors, torch.Tensor) else tuple(tensors)
    inputs = (inputs,) if isinstance(inputs, torch.Tensor) else \
        tuple(inputs) if inputs is not None else tuple()
​
    # _make_grads 把 grad_tensors 中的元素重新组织成tuple(list(torch.Tensor, ...))的形式
    grad_tensors_ = _tensor_or_tensors_to_tuple(grad_tensors, len(tensors))
    grad_tensors_ = _make_grads(tensors, grad_tensors_)
    if retain_graph is None:
        retain_graph = create_graph
​
    # 执行后向传播
    Variable._execution_engine.run_backward(
        tensors, grad_tensors_, retain_graph, create_graph, inputs,
        allow_unreachable=True, accumulate_grad=True)  # allow_unreachable flag

Variable._execution_engine.run_backwardЗдесь начинается вход в мир C++.

                                Python      +      C++
                                            |
                                            |
                                            |
backward                                    |
    +                                       |
    |                                       |
    |                                       |
    |                                       |
    v                                       |
Variable._execution_engine.run_backward +---------->
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            |
                                            +

2.2 Двигатель

В файле torch/autograd/variable.py создается _execution_engine.

from torch._C import _ImperativeEngine as ImperativeEngine
​
Variable._execution_engine = ImperativeEngine()

отtorch/_C/__init__.pyi.inМы видим, что мир C++ должен обратиться к python_engine.cpp за ответами.

# Defined in torch/csrc/autograd/python_engine.cpp
class _ImperativeEngine:

0x03 С++ мир

После входа в мир C++ давайте замедлимся и сначала вспомним систему поддержки, иначе мы запутаемся, потому что это слишком сложно.

3.1 Система поддержки

3.1.1 Edge

Edge представляет ребро в графе с помощью пары функций input_nr.

using tensor_list = std::vector<at::Tensor>;
using variable_list = std::vector<Variable>;
using edge_list = std::vector<Edge>;
using saved_variable_list = std::vector<SavedVariable>;
using IndexRange = std::pair<size_t, size_t>;
​
/// Represents a particular input of a function.
struct Edge {
  Edge() noexcept : function(nullptr), input_nr(0) {}
​
  Edge(std::shared_ptr<Node> function_, uint32_t input_nr_) noexcept
      : function(std::move(function_)), input_nr(input_nr_) {}
​
  /// The function this `Edge` points to.
  std::shared_ptr<Node> function; // 本边指向的Node
​
  /// The identifier of a particular input to the function.
  uint32_t input_nr; //指定本Edge在后向传播之中是function的第几个输入 
};
}} // namespace torch::autograd
​

3.1.2 Функции, связанные с границей

torch/csrc/autograd/function.h Вот функции, связанные с краем. Оба являются функциями класса Node.

  void set_next_edge(size_t index, Edge edge) {
    update_topological_nr(edge);
    next_edges_[index] = std::move(edge);
  }
​
  void add_next_edge(Edge edge) {
    update_topological_nr(edge);
    next_edges_.push_back(std::move(edge));
  }
​
  void set_next_edges(edge_list&& next_edges) {
    next_edges_ = std::move(next_edges);
    for(const auto& next_edge : next_edges_) {
      update_topological_nr(next_edge);
    }
  }
​
  const Edge& next_edge(size_t index) const noexcept {
    return next_edges_[index];
  }
​
  const edge_list& next_edges() const noexcept {
    return next_edges_;
  }
​
  edge_list& next_edges() noexcept {
    return next_edges_;
  }
​
  uint32_t num_outputs() const noexcept {
    return next_edges_.size();
  }

В torch/csrc/jit/runtime/graph_executor.cpp также есть некоторые функции, связанные с периферией.

void addOutputForTensor(const at::Tensor& tensor) {
  auto v = Variable(tensor);
  add_next_edge(
      v.defined() ? torch::autograd::impl::gradient_edge(v)
                  : autograd::Edge{});
}
​
void addOutputForIValue(const IValue& value) {
  if (value.isTensorList()) {
    for (const at::Tensor tensor : value.toTensorList()) {
      addOutputForTensor(tensor);
    }
  } else if (value.isTensor()) {
    addOutputForTensor(value.toTensor());
  } else {
    // We could have None passed here via `Optional[Tensor]`
    add_next_edge(autograd::Edge{});
  }
}

Gradient_edge будет использоваться в предыдущей и следующей статьях, в которых используется градиент переменной и результат прямого распространения для построения ребра.

Edge gradient_edge(const Variable& self) {
  // If grad_fn is null (as is the case for a leaf node), we instead
  // interpret the gradient function to be a gradient accumulator, which will
  // accumulate its inputs into the grad property of the variable. These
  // nodes get suppressed in some situations, see "suppress gradient
  // accumulation" below. Note that only variables which have `requires_grad =
  // True` can have gradient accumulators.
    
  // self.grad_fn() 这里触发了一个调用
  if (const auto& gradient = self.grad_fn()) { // 这是一个中间节点,gradient 是一个Function,比如可以得到一个SubBackward0实例
    return Edge(gradient, self.output_nr()); // self.output_nr() 表示本Edge是function的第n个输入。前向传播时候的第 n 个输出在反向传播时候就是第 n 个输入。
  } else {
    return Edge(grad_accumulator(self), 0); // 这是一个叶子节点,所以生成一个AccumulateGrad,0表示本Edge是function的第一个输入
  }
}

3.1.3 Расширения Python

Далее мы представим расширения Python. Вообще говоря, люди не пишут модули Python непосредственно на C, а пишут модули C напрямую, а затем оборачивают их, чтобы Python можно было вызывать напрямую.Процесс примерно выглядит следующим образом:

  1. Язык C представляет заголовочный файл Python.h.
  2. Напишите функцию-оболочку, которая обрабатывает аргументы, переданные из мира Python.
  3. Язык C реализует функциональную логику.
  4. Оберните возвращаемое значение языка C в объект Python.
  5. Зарегистрируйте необходимые функции в структуре PyMethodDef.
  6. Зарегистрируйте имя модуля в методе инициализации.
  7. Скомпилируйте исходные файлы C в связанные библиотеки для использования Python.

PyMethodDef определяется следующим образом:

\

typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

3.2 Введение

3.2.1 Инициализация

В torch/csrc/Module.cpp initModule инициализирует мир C++. Это огромная функция, в этой статье мы сосредоточимся только на THPFunction_initModule и THPEngine_initModule, опуская много кода.

PyObject* initModule() {
​
  ......
      
  ASSERT_TRUE(THPFunction_initModule(module));
  ASSERT_TRUE(THPEngine_initModule(module));
​
  ......
    
}
3.2.1.1 Инициализация иерархии наследования

Во время инициализации THPFunction_initModule(модуль) создалtorch._C._FunctionBase.

bool THPFunction_initModule(PyObject *module)
{
  if (PyType_Ready(&THPFunctionType) < 0)
    return false;
  Py_INCREF(&THPFunctionType);
  
  // 创建了`torch._C._FunctionBase`
  PyModule_AddObject(module, "_FunctionBase", (PyObject *)&THPFunctionType);
  return true;
}

И в torch/autograd/function.py есть следующие два класса, начинающиеся сtorch._C._FunctionBaseдля базового класса:

class Function(with_metaclass(FunctionMeta, _C._FunctionBase, _ContextMethodMixin, _HookMixin))
class BackwardCFunction(_C._FunctionBase, _ContextMethodMixin, _HookMixin)
3.2.2.2 Инициализация двигателя

THPEngine_initModule(модуль) созданtorch._C._EngineBase,_EngineBaseЭтот класс отвечает за предварительную обработку перед выполнением динамического графа,_EngineBaseОн предварительно обрабатывает запросы, такие как torch.autograd, и отправляет их на реальный движок для выполнения..

PyObject* initModule() {
  ......
  ASSERT_TRUE(THPVariable_initModule(module)); 
  ASSERT_TRUE(THPFunction_initModule(module));
  ASSERT_TRUE(THPEngine_initModule(module)); // 这里初始化引擎
}

THPEngine_initModule через функциюPyModule_AddObjectЗарегистрируйте объект THPEngineType в модуле модуля (тип PyObject) и назовите его_ImperativeEngine. Соответствует стороне Python_ImperativeEngine.

bool THPEngine_initModule(PyObject *module)
{
#ifndef _WIN32
  if (pthread_atfork(nullptr, nullptr, child_atfork) != 0) {
    throw std::runtime_error("unable to set pthread_atfork handler");
  }
#endif
  if (PyType_Ready(&THPEngineType) < 0)
    return false;
  Py_INCREF(&THPEngineType);
  
  // 为 Python 注册了引擎
  PyModule_AddObject(module, "_ImperativeEngine", (PyObject *)&THPEngineType);
  set_default_engine_stub(python::PythonEngine::get_python_engine);
  return true;
}

THPEngineType определяется следующим образом, видно, что сгенерированный экземпляр "torch._C._EngineBase".

PyTypeObject THPEngineType = {
  PyVarObject_HEAD_INIT(nullptr, 0)
  "torch._C._EngineBase",                      /* tp_name */
  sizeof(THPEngine),                           /* tp_basicsize */
  0,                                           /* tp_itemsize */
  nullptr,                                     /* tp_dealloc */
  0,                                           /* tp_vectorcall_offset */
  nullptr,                                     /* tp_getattr */
  nullptr,                                     /* tp_setattr */
  nullptr,                                     /* tp_reserved */
  nullptr,                                     /* tp_repr */
  nullptr,                                     /* tp_as_number */
  nullptr,                                     /* tp_as_sequence */
  nullptr,                                     /* tp_as_mapping */
  nullptr,                                     /* tp_hash  */
  nullptr,                                     /* tp_call */
  nullptr,                                     /* tp_str */
  nullptr,                                     /* tp_getattro */
  nullptr,                                     /* tp_setattro */
  nullptr,                                     /* tp_as_buffer */
  Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,    /* tp_flags */
  nullptr,                                     /* tp_doc */
  nullptr,                                     /* tp_traverse */
  nullptr,                                     /* tp_clear */
  nullptr,                                     /* tp_richcompare */
  0,                                           /* tp_weaklistoffset */
  nullptr,                                     /* tp_iter */
  nullptr,                                     /* tp_iternext */
  THPEngine_methods,                           /* tp_methods */
  nullptr,                                     /* tp_members */
  nullptr,                                     /* tp_getset */
  nullptr,                                     /* tp_base */
  nullptr,                                     /* tp_dict */
  nullptr,                                     /* tp_descr_get */
  nullptr,                                     /* tp_descr_set */
  0,                                           /* tp_dictoffset */
  nullptr,                                     /* tp_init */
  nullptr,                                     /* tp_alloc */
  THPEngine_new                                /* tp_new */
};

3.2.3 Связь с миром Python

Теперь, когда движок C++ связан с движком Python, давайте посмотрим на конкретные функции движка.

заtorch._C._EngineBase, функцией-членом которого является THPEngine_methods. Тип THPEngine_methods — это PyMethodDef, который мы представили ранее, который используется для расширений Python. Здесь определены run_backward, queue_callback и is_checkpoint_valid.Напомним, run_backward — это точка входа в мир Python..

static struct PyMethodDef THPEngine_methods[] = {
  {(char*)"run_backward",
    castPyCFunctionWithKeywords(THPEngine_run_backward), // 与Python对应
    METH_VARARGS | METH_KEYWORDS, nullptr},
  {(char*)"queue_callback", THPEngine_queue_callback, METH_O, nullptr},
  {(char*)"is_checkpoint_valid", THPEngine_is_checkpoint_valid, METH_NOARGS, nullptr},
  {nullptr}
};

Согласно предыдущему определению PyMethodDef: «run_backward» — это имя метода, а THPEngine_run_backward — соответствующий метод языка C. Итак, мир PythonVariable._execution_engine.run_backwardОн соответствует THPEngine_run_backward.

                                                      Python      +      C++
                                                                  |
                                                                  |                     initModule
                                                                  |                          +
                                                                  |                          |
                                                                  |                          |
                                                                  |                          |
                                                                  |                          v
                   backward                                       |                   THPEngine_initModule
                       +                                          |                          +
                       |                                          |                          |
                       |                                          |                          |
                       |                                          |                          |
                       v                                          |                          v
                   Variable._execution_engine.run_backward        |   PyModule_AddObject(module, "_ImperativeEngine", &THPEngineType)
                                               +                  |                          +
                                               |                  |                          |
                                               |                  |                          |
                                               |                  |                          v
                                               |                  |
                                               |                  |       +----------------------------------------------------------+
                                               v                  |       | module                                                   |
                                                                  |       |                                                          |
                                 +-------------------------+      |       |   +---------------------------------------------------+  |
                                 | _ImperativeEngine       |      |       |   | _ImperativeEngine                                 |  |
Variable._execution_engine +---> |                         |      |       |   |                                                   |  |
                                 |                         |      |       |   |  +----------------------------------------------+ |  |
                                 |                         |      |       |   |  | THPEngine_methods                            | |  |
                                 |                         |      |       |   |  |                                              | |  |
                                 |                         |      |       |   |  |                                              | |  |
                                 |        run_backward +----------------------------->  "run_backward" : THPEngine_run_backward | |  |
                                 |                         |      |       |   |  |                                              | |  |
                                 |                         |      |       |   |  +----------------------------------------------+ |  |
                                 +-------------------------+      |       |   |                                                   |  |
                                                                  |       |   +---------------------------------------------------+  |
                                                                  |       |                                                          |
                                                                  +       +----------------------------------------------------------+
​
​

Телефон такой:

img

Итак, нам нужно проанализировать THPEngine_run_backward в мире C++.

3.3 Вход в движок C++

THPEngine_run_backward — это точка входа движка C++, расположенная по адресу: torch/csrc/autograd/python_engine.cpp.

Основная логика следующая:

  • Во-первых, через функциюPyArg_ParseTupleAndKeywordsПовторно проанализируйте входные параметры и назначьте их вновь определенным переменным:

    • Новые переменные:tensors,grad_tensors,keep_graph,create_graph,inputsа такжеallow_unreachable. Например, inputs — это вектор.
    • Входными данными в мире Python являются torch.autograd.backward(tensors, grad_tensors), и эти параметры преобразуются в переменные tensors и grad_tensors в мире C++ соответственно. Тип этих двух переменных в C++ — PyObject, а размер — 1. PyObject — это базовый класс для любого объекта Python, в этом методе тензоры и grad_tensors фактически являются экземплярами класса THPVariable.
  • Получите входной тензор и градиентный тензор из входных данных, в основном для проверки согласованности типа переменной и размера кортежа тензоров и grad_tensors.

  • Из входных данных строятся три переменныеedge_list roots,output_edgesиvariable_list grads, эти три являются отправной точкой обратного распространения (деривации), дополнительной информацией и градиентом конечного вывода модели.

    • roots — это градиент_edge(), содержащий выходной узел прямого распространения (т. е. выходной узел(grad_fn_, 0)) вектор. требует внимания,grad_fn_является производным классом от Node, поэтому roots — это Node.
    • grads — это градиенты, генерируемые прямым распространением, если они не настроены, они инициализируются как (tensor(1.),).
    • output_edges — это выходные ребра обратного распространения, построенные из входных данных входного узла прямого распространения.
  • Вызовите outputs = engine.execute(roots, grads, keep_graph, create_graph, output_edges), чтобы официально войти в механизм обратного распространения.

Конкретный код выглядит следующим образом:

// Implementation of torch._C._EngineBase.run_backward
PyObject *THPEngine_run_backward(PyObject *self, PyObject *args, PyObject *kwargs)
{
  HANDLE_TH_ERRORS
  PyObject *tensors = nullptr;
  PyObject *grad_tensors = nullptr;
  unsigned char keep_graph = 0;
  unsigned char create_graph = 0;
  PyObject *inputs = nullptr;
  unsigned char allow_unreachable = 0;
  unsigned char accumulate_grad = 0; // Indicate whether to accumulate grad into leaf Tensors or capture
  const char *accepted_kwargs[] = { // NOLINT
      "tensors", "grad_tensors", "keep_graph", "create_graph", "inputs",
      "allow_unreachable", "accumulate_grad", nullptr
  };
    
  // 对输入的参数重新解析并赋值给新定义的变量tensors,grad_tensors等等,比如 inputs就是一个vector 
  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OObb|Obb", (char**)accepted_kwargs,
        &tensors, &grad_tensors, &keep_graph, &create_graph, &inputs, &allow_unreachable, &accumulate_grad))
    return nullptr;

  // 从输入获取输入张量和梯度张量,主要是检查tensors和grad_tensors的变量类型以及tuple size是否一致。 
  Py_ssize_t num_tensors = PyTuple_GET_SIZE(tensors);
  Py_ssize_t num_gradients = PyTuple_GET_SIZE(grad_tensors);
  THPUtils_assert(num_tensors == num_gradients, "got %ld tensors and %ld "
      "gradients", num_tensors, num_gradients);

  // The user either called autograd.backward(...) or autograd.grad(...) to get here
  bool backward_api_called = accumulate_grad;

  // 我们回忆一下定义
  // using variable_list = std::vector<Variable>;
  // using edge_list = std::vector<Edge>;
  edge_list roots; // 就是反向传播的起点(根节点)
  roots.reserve(num_tensors);
  variable_list grads; // 就是反向传播的梯度
  grads.reserve(num_tensors);
    
  // 依据输入来配置roots和grads  
  for (int i = 0; i < num_tensors; i++) {
    // tensors是输入节点,即前向传播图的输出  
    PyObject *_tensor = PyTuple_GET_ITEM(tensors, i);
    THPUtils_assert(THPVariable_Check(_tensor), "element %d of tensors "
	  // 得到 gradient_edge = Edge(grad_fn(), output_nr())
    auto gradient_edge = torch::autograd::impl::gradient_edge(variable);
    roots.push_back(std::move(gradient_edge)); // root增加一个Edge

    PyObject *grad = PyTuple_GET_ITEM(grad_tensors, i);
    if (THPVariable_Check(grad)) {
      const Variable& grad_var = THPVariable_Unpack(grad);
      if (grad_var.has_names()) {
        TORCH_WARN(
            "Autograd was passed a named grad tensor with dims ", grad_var.names(),
            ". Autograd does not yet support named tensor semantics, so all names ",
            "will be ignored. In practice all computed gradients will still be correct "
            "according to regular tensor semantics.");
      }
      grads.push_back(grad_var); // 增加一个梯度
    } 
  }

  // 构建一个输出Edge列表                 
  std::vector<Edge> output_edges;
  if (inputs != nullptr) {
    int num_inputs = PyTuple_GET_SIZE(inputs);
    output_edges.reserve(num_inputs);
    // 遍历输入列表  
    for (int i = 0; i < num_inputs; ++i) {
      PyObject *input = PyTuple_GET_ITEM(inputs, i);
      const auto& tensor = THPVariable_Unpack(input);
      const auto output_nr = tensor.output_nr();
      auto grad_fn = tensor.grad_fn();
      if (!grad_fn) {
        // 获取 grad_accumulator,用来判断是否是叶子节点  
        grad_fn = torch::autograd::impl::try_get_grad_accumulator(tensor);
      }

      if (!grad_fn) {
        // NOTE [ Autograd Unreachable Input ]
        // Since input has no grad_accumulator, its guaranteed to be unreachable.
        // We initialize an edge pointing to a non-nullptr Node so nodes in the graph
        // (e.g., mul when an operand is scalar) that have edges pointing to nullptr
        // don't get erroneously assigned `needed = True` in exec_info.
        // 说明是叶子节点  
        output_edges.emplace_back(std::make_shared<Identity>(), 0);
      } else {
        // 是中间节点  
        output_edges.emplace_back(grad_fn, output_nr);
      }
    }
  }

  // 现在,roots是包含有(前向传播输出节点的grad_fn_, 0)的vector。
  // grads 是前向传播产生的梯度,如果没有配置,则初始化为(tensor(1.),)
  // output_edges 是依据前向传播输入节点 input 构建的后向传播输出边                  
  variable_list outputs;
  {
    pybind11::gil_scoped_release no_gil;
    auto& engine = python::PythonEngine::get_python_engine();
    // 进入引擎执行  
    outputs = engine.execute(roots, grads, keep_graph, create_graph, accumulate_grad, output_edges);
  }

  if (!backward_api_called && inputs != nullptr) {
    int num_inputs = PyTuple_GET_SIZE(inputs);
    THPObjectPtr py_outputs {PyTuple_New(num_inputs)};
    if (!py_outputs) return nullptr;
    for (int i = 0; i < num_inputs; i++) {
      PyTuple_SET_ITEM(py_outputs.get(), i, THPVariable_Wrap(outputs[i]));
    }
    return py_outputs.release();
  } else {
    Py_RETURN_NONE;
  }
  END_HANDLE_TH_ERRORS
}

Далее мы проанализируем несколько вспомогательных функций, используемых THPEngine_run_backward.

3.3.1 try_get_grad_accumulator

В приведенном выше коде есть grad_fn = torch::autograd::impl::try_get_grad_accumulator(tensor) для получения метода вычисления градиента. На самом деле, он используется, чтобы судить, является ли это листовым узлом, только нелистовой узел grad_accumulator_ не пуст.

try_get_grad_accumulator возвращает указатель наNodeуказатель на объект:std::weak_ptr<Node> grad_accumulator_. как рассчитать градиент.

Конкретная логика такова:

  • сначала передать функциюget_autograd_metaвернутьAutogradMetaструктура.
  • Затем получите доступ к переменным-членам в структуреgrad_accumulator_grad_accumulator_это указатель типаNodeобъектstd::weak_ptrуказатель.
  • наконец прошлоlock()функция для созданияstd::shared_ptrдля управления объектами.
  std::shared_ptr<Node> try_get_grad_accumulator(const Variable& self) {
    if (get_autograd_meta(self)) {
      return get_autograd_meta(self)->grad_accumulator_.lock();
    } else {
      return nullptr;
    }
  }

3.3.2 gradient_edge

В приведенном выше коде градиент_края используется для построения края на основе входного тензора.

auto gradient_edge = torch::autograd::impl::gradient_edge(variable);
roots.push_back(std::move(gradient_edge)); // root增加一个Edge

градиент_край выглядит следующим образом:

Edge gradient_edge(const Variable& self) {
  // If grad_fn is null (as is the case for a leaf node), we instead
  // interpret the gradient function to be a gradient accumulator, which will
  // accumulate its inputs into the grad property of the variable. These
  // nodes get suppressed in some situations, see "suppress gradient
  // accumulation" below. Note that only variables which have `requires_grad =
  // True` can have gradient accumulators.
  if (const auto& gradient = self.grad_fn()) {
    return Edge(gradient, self.output_nr());
  } else {
    return Edge(grad_accumulator(self), 0);
  }
}

3.3.3 output_edges

В приведенном выше коде std::vector output_edges создает список выходных ребер.

После получения grad_accumulator_ ему будет присвоено значение grad_fn, которое используется для определения того, является ли он конечным узлом. Затем создайте листовые узлы и промежуточные узлы соответственно и поместите их в output_edges.

      if (!grad_fn) {
        // NOTE [ Autograd Unreachable Input ]
        // Since input has no grad_accumulator, its guaranteed to be unreachable.
        // We initialize an edge pointing to a non-nullptr Node so nodes in the graph
        // (e.g., mul when an operand is scalar) that have edges pointing to nullptr
        // don't get erroneously assigned `needed = True` in exec_info.
        output_edges.emplace_back(std::make_shared<Identity>(), 0); // 叶子节点
      } else {
        output_edges.emplace_back(grad_fn, output_nr); // 非叶子节点
      }

Давайте посмотрим на переменные grad_fn и output_nr, которые создают output_edges, чтобы увидеть, откуда они берутся.

grad_fnуказатель, полученный методом try_get_grad_accumulatorNodeобъектstd::shared_ptrУказатель — это операция вычисления градиента.

output_pr устанавливается следующим образом, а конечным результатом является переменная-член uint32_t output_nr_ в структуре AutogradMeta.

const auto output_nr = tensor.output_nr();

emplace_back()Функция добавляет в контейнер временный объект, временный объект конструируется на месте, операции присваивания или перемещения не выполняются.

Вспомним определение края. Так что видно, чтоemplace_back()Он использует эти входные данные для создания Edge.

/// Represents a particular input of a function.
struct Edge {
  Edge() noexcept : function(nullptr), input_nr(0) {}
  Edge(std::shared_ptr<Node> function_, uint32_t input_nr_) noexcept
      : function(std::move(function_)), input_nr(input_nr_) {}

  /// The function this `Edge` points to.
  std::shared_ptr<Node> function; // 指向的Node

  /// The identifier of a particular input to the function.
  uint32_t input_nr; //指定本Edge在后向传播之中是function的第几个输入 
};

Преобразование ввода показано на рисунке ниже.Вы можете видеть, как ввод преобразуется из Python и, наконец, передается в механизм C++.В качестве примера возьмем следующие переменные:

  • Тензоры Python преобразуются в корни C++.
  • grad_tensor Python преобразуются в градации C++.
  • Входные данные Python преобразуются в output_edges C++.
  • Наконец, передайте эти три переменные в движок: PythonEngine.execute(roots, grads, keep_graph, create_graph, accumulation_grad, output_edges).
                  backward(tensors, grad_tensors, inputs)
                              +             +        +
                              |             |        |
Python                        |             |        |
                              |             |        |
+------------------------------------------------------------------------------------------+
                              |             |        |
C++   THPEngine_run_backward  |             |        |
                              |             |        +-----------------------------+
                              |             |                                      |
                              |             |                                      |
                              |             +-----------------------------+        |
                              v                                           |        |
                                                                          |        |
+------root = [(tensor_1.grad_fn_, 0),...,(tensor_n.grad_fn_, 0)]         |        |
|                                                                         |        |
|                                                                         |        |
|                                                                         |        |
|   +--grads = [grad_tensor_1,...,grad_tensor_n  ] <----------------------+        |
|   |                                                                              |
|   |                                                                              |
|   |                                                                              v
|   |  output_edges = [(input_1.grad_fn_, output_nr_1),...,(input_n.grad_fn_, output_nr_n)]
|   |                                                                              +
|   +-------------------------+                                                    |
|                             |                                                    |
|                             |                                                    |
+----------------------+      |                                                    |
                       |      |                                                    |
                       v      v                                                    v

PythonEngine.execute(roots, grads, keep_graph, create_graph, accumulate_grad, output_edges)

3.4 PythonEngine

Предыдущий код THPEngine_run_backward выглядит следующим образом, мы можем видеть, что THPEngine_run_backward, наконец, вызывает логику обработки PythonEngine.

auto& engine = python::PythonEngine::get_python_engine();
// 进入引擎执行  
outputs = engine.execute(roots, grads, keep_graph, create_graph, accumulate_grad, output_edges);

3.4.1 Получить двигатель

get_python_engine определяет здесь статическую переменную. Вся программа PyTorch глобально поддерживает только один экземпляр Engine, то есть экземпляр PythonEngine.

Engine& PythonEngine::get_python_engine() {
  static PythonEngine engine;
  // This is "probably" thread-safe because the flag is set in a fork handler
  // before any threads are created, and this function is only called with the
  // GIL held. However, using fork + threads is playing with fire so this is
  // more of a "best effort" thing. For example, if the fork occurs while the
  // backwards threads hold a lock, we'll probably deadlock in the engine
  // destructor.
  if (_reinitialize_engine) {
    engine.release_workers();
    engine.~PythonEngine();
    new (&engine) torch::autograd::python::PythonEngine();
    _reinitialize_engine = false;
  }
  return engine;
}

3.4.2 Определения

Итак, давайте взглянем на определение PythonEngine. PythonEngine является производным классом от Engine, что эквивалентно его инкапсуляции. Он в основном настроен для характеристик мира Python. Например, подкласс PythonEngine переписывает выполнение родительского класса и переводит исключения C++ в исключения Python. Основная работа по-прежнему выполняется базовым классом Engine:

struct PythonEngine : public Engine {
  static Engine& get_python_engine();
  ~PythonEngine() override;
  void thread_init(int device,
      const std::shared_ptr<ReadyQueue>& ready_queue,
      bool should_increment) override;
  void thread_on_exception(
      std::shared_ptr<GraphTask> graph_task,
      const std::shared_ptr<Node>& fn,
      std::exception& e) override;
  variable_list execute(
      const edge_list& roots,
      const variable_list& inputs,
      bool keep_graph,
      bool create_graph,
      bool accumulate_grad,
      const edge_list& outputs = {}) override;
​
  std::shared_ptr<at::ivalue::Future> execute_with_graph_task(
      const std::shared_ptr<GraphTask>& graph_task,
      std::shared_ptr<Node> graph_root,
      InputBuffer&& input_buffer) override;
​
  std::unique_ptr<AnomalyMetadata> make_anomaly_metadata() override;
  private:
    PythonEngine();
};

Код выполнения выглядит следующим образом, поэтому, начиная со следующего, мы должны увидеть, как работает Engine.

variable_list PythonEngine::execute(
    const edge_list& roots,
    const variable_list& inputs,
    bool keep_graph,
    bool create_graph,
    bool accumulate_grad,
    const edge_list& outputs) {
  try {
    return Engine::execute(roots, inputs, keep_graph, create_graph, accumulate_grad, outputs);
  } catch (python_error& e) {
    e.restore();
    throw;
  }
}

Текущая логика расширяется следующим образом:

                  backward(tensors, grad_tensors, inputs)
                              +             +        +
                              |             |        |
Python                        |             |        |
                              |             |        |
+------------------------------------------------------------------------------------------+
                              |             |        |
C++   THPEngine_run_backward  |             |        |
                              |             |        +-----------------------------+
                              |             |                                      |
                              |             |                                      |
                              |             +-----------------------------+        |
                              v                                           |        |
                                                                          |        |
+------root = [(tensor_1.grad_fn_, 0),...,(tensor_n.grad_fn_, 0)]         |        |
|                                                                         |        |
|                                                                         |        |
|                                                                         |        |
|   +--grads = [grad_tensor_1,...,grad_tensor_n  ] <----------------------+        |
|   |                                                                              |
|   |                                                                              |
|   |                                                                              v
|   |  output_edges = [(input_1.grad_fn_, output_nr_1),...,(input_n.grad_fn_, output_nr_n)]
|   |                                                                              +
|   +-------------------------+                                                    |
|                             |                                                    |
|                             |                                                    |
+----------------------+      |                                                    |
                       |      |                                                    |
                       v      v                                                    v

PythonEngine.execute(roots, grads, keep_graph, create_graph, accumulate_grad, output_edges)
               +       +       +                                                   +
               |       |       |                                                   |
               |       |       |                                                   |
               v       v       v                                                   v
     Engine::execute(roots, inputs, keep_graph, create_graph, accumulate_grad, outputs)

Телефон такой:

img

3.5 Другой способ вызова

Наконец, мы вставляем еще один run_backward для анализа.

run_backward находится в torch/csrc/autograd/autograd.cpp. Это должно быть специально для нужд прямых вызовов в мире C++, что отличается от наших предыдущих окольных путей через Python.

void backward(
    const variable_list& tensors,
    const variable_list& grad_tensors,
    c10::optional<bool> retain_graph,
    bool create_graph,
    const variable_list& inputs) {
  variable_list gradients = _make_grads(tensors, grad_tensors);
  if (!retain_graph) {
    retain_graph = create_graph;
  }
  run_backward(tensors, gradients, retain_graph.value(), create_graph, inputs, /*allow_unused=*/true, /*accumulate_grad=*/true);
}
​
variable_list grad(
    const variable_list& outputs,
    const variable_list& inputs,
    const variable_list& grad_outputs,
    c10::optional<bool> retain_graph,
    bool create_graph,
    bool allow_unused) {
  variable_list gradients = _make_grads(outputs, grad_outputs);
  if (!retain_graph) {
    retain_graph = create_graph;
  }
  return run_backward(
    outputs, gradients, retain_graph.value(), create_graph, inputs, allow_unused, /*accumulate_grad=*/false);
}

run_backward также вызывает Engine::get_default_engine().execute в конце.

variable_list run_backward(
    const variable_list& outputs,
    const variable_list& grad_outputs,
    bool keep_graph,
    bool create_graph,
    const variable_list& inputs,
    bool allow_unused,
    bool accumulate_grad) {
  size_t num_tensors = outputs.size();
  edge_list roots;
  roots.reserve(num_tensors);
  for (size_t i = 0; i < num_tensors; i++) {
    const Variable& output = outputs[i];
    auto gradient_edge = impl::gradient_edge(output);
    roots.push_back(std::move(gradient_edge));
  }
​
  edge_list output_edges;
  if (!inputs.empty()) {
    size_t num_inputs = inputs.size();
    output_edges.reserve(num_inputs);
    for (size_t i = 0; i < num_inputs; ++i) {
      const Variable& input = inputs[i];
      const auto output_nr = input.output_nr();
      auto grad_fn = input.grad_fn();
      if (!grad_fn) {
        grad_fn = impl::try_get_grad_accumulator(input);
      }
      if (!grad_fn) {
        // See NOTE [ Autograd Unreachable Input ] for details
        output_edges.emplace_back(std::make_shared<Identity>(), 0);
      } else {
        output_edges.emplace_back(grad_fn, output_nr);
      }
    }
  }
​
  // 调用了引擎代码
  variable_list grad_inputs = Engine::get_default_engine().execute(
      roots, grad_outputs, keep_graph, create_graph, accumulate_grad, output_edges);
  // check if grad_inputs contains None or not base on the allow_unused flag
  if (!inputs.empty() && !allow_unused) {
    size_t num_inputs = inputs.size();
    for (size_t i = 0; i < num_inputs; ++i) {
      TORCH_CHECK(
          grad_inputs[i].defined(),
          "One of the "
          "differentiated Tensors appears to not have been used "
          "in the graph. Set allow_unused=True if this is the "
          "desired behavior.");
    }
  }
  return grad_inputs;
}
​

На этом анализ вызывающего процесса завершен, и ядром является вызов функции движка для обработки, поэтому мы приступим к анализу движка в следующей статье.

0xEE Личная информация

★★★★★★Думая о жизни и технологиях★★★★★★

Публичный аккаунт WeChat:мысли Росси

ссылка 0xFF

Написание модулей Python на C