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 можно было вызывать напрямую.Процесс примерно выглядит следующим образом:
- Язык C представляет заголовочный файл Python.h.
- Напишите функцию-оболочку, которая обрабатывает аргументы, переданные из мира Python.
- Язык C реализует функциональную логику.
- Оберните возвращаемое значение языка C в объект Python.
- Зарегистрируйте необходимые функции в структуре PyMethodDef.
- Зарегистрируйте имя модуля в методе инициализации.
- Скомпилируйте исходные файлы 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 | | |
| | | | | | | | |
| | | | | +----------------------------------------------+ | |
+-------------------------+ | | | | |
| | +---------------------------------------------------+ |
| | |
+ +----------------------------------------------------------+
Телефон такой:
Итак, нам нужно проанализировать 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 — это выходные ребра обратного распространения, построенные из входных данных входного узла прямого распространения.
- roots — это градиент_edge(), содержащий выходной узел прямого распространения (т. е. выходной узел
-
Вызовите 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)
Телефон такой:
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:мысли Росси