0x00 сводка
В этой серии будет проанализировано, как реализована функция автоматической дифференциации PyTorch, примерно в десяти статьях. Эта статья является третьей частью прямого распространения и знакомит с конкретным механизмом реализации.
Во время обратного распространения, когда тензор получен, движку необходимо знать:
- Как вызвать вычисление градиента на этом тензоре, т.е. где найти функцию F, которая вычисляет градиент.
- После получения функции F входом функции является сам тензор, но функции F необходимо знать некоторую метаинформацию входного параметра (этого тензора), такую как тип, форма, устройство.
- После того, как F вычислит градиент, ему необходимо знать, куда следует распространять выходные данные F, то есть как перейти к следующему шагу на графе расчета обратного распространения.
Эта статья представляет собой конкретный анализ того, как эта информация устанавливается в прямом распространении.
Первые несколько статей из этой серии связаны ниже:
Автоматическая дифференциация инструментов глубокого обучения (1)
Автоматическая дифференциация инструментов глубокого обучения (2)
Автоматическая дифференциация оружия глубокого обучения (3) --- Пример интерпретации
[Анализ исходного кода] Как PyTorch реализует прямое распространение (1) --- Базовый класс (1)
[Анализ исходного кода] Как PyTorch реализует прямое распространение (2) --- Базовый класс (ниже)
0x01 Вычислительный график
1.1 Родственные классы графов
Граф вычислений - это ориентированный граф, узлы которого представляют собой реализованные операторы или данные (конечные узлы).Направление стрелки указывает направление потока данных от входного узла к выходному узлу. Как видно из предыдущих глав, есть три основных класса, связанных с графами: Node, Edge и Engine (мы проанализируем Engine позже).
-
Узел — это класс Node, представляющий операцию.
- Каждый узел получает 0 или более переменных и выводит 0 или более переменных. Узлы связаны Edge, что на самом деле осуществляется через переменные-члены Node.
next_edges_
связанный. - Все функции обратного распространения наследуются от Node, например, SubBackward0 наследуется от Node.
- Каждый узел получает 0 или более переменных и выводит 0 или более переменных. Узлы связаны Edge, что на самом деле осуществляется через переменные-члены Node.
-
Край Edge на самом деле (Node, input_nr)
- Функция переменной-члена std::shared_ptr Edge: указывает Node, на который указывает это ребро.
- Переменная-член Edge uint32_t input_nr : указывает, какие входные данные этого ребра являются первыми для функции.
-
Элемент next_edges_ of Node — это набор экземпляров Edge, представляющих (другой) Node, на который должно быть выведено возвращаемое значение этого экземпляра Node, то есть next_edges_ — это связь между Node и Node. Переменные перемещаются между этими ребрами при выполнении графа вычислений.
-
Двигатель — это исполнительный механизм.
1.2 Динамический график
Pytorch использует подход динамического вычислительного графа в своем дизайне. Динамический означает, что вычислительный граф обратного распространения динамически обновляется. В начале каждого раунда обратного распространения (после окончания прямого распространения) вычислительный граф будет динамически перестраиваться, после завершения этого обратного распространения граф уничтожает вычислительный граф и освобождается в памяти. Если вы хотите использовать его снова в новом раунде, вы должны построить его снова с нуля. Это динамическое обновление позволяет пользователю изменять форму и размер сети во время итеративного процесса.
Следующий код может видеть характеристики динамических графиков.
# 第一遍,生成动态图
a = torch.tensor(2., requires_grad=True)
b = torch.tensor(6., requires_grad=True)
Q = 3*a**3 - b**2
external_grad = torch.tensor(1.)
Q.backward(gradient=external_grad) # 正常
Q.backward(gradient=external_grad) # RuntimeError
# 第二次:再来一遍
a = torch.tensor(2., requires_grad=True)
b = torch.tensor(6., requires_grad=True)
Q = 3*a**3 - b**2
external_grad = torch.tensor(1.)
Q.backward(gradient=external_grad) # 正常
1.3 Динамический дисплей
Ниже приведена официальная динамическая карта PyTorch, вы можете получить визуальное представление.
Для лучшего представления давайте разберем анимацию.
Во-первых, объявить некоторые тензоры.
Затем умножьте две матрицы.
Умножьте две другие матрицы
Затем сложите два результата умножения.
Добавлена функция активации Tanh.
Добавьте функцию потерь.
Обратное распространение, вычисление градиентов.
Из этого видно, что связь динамического графа строится в процессе прямого расчета.
0x02 Общий анализ
Мы продолжаем дорабатывать приведенный выше пример кода, чтобы увидеть различные тензоры на графе вычислений:
a = torch.tensor(2., requires_grad=True)
b = torch.tensor(6., requires_grad=True)
X = a ** 3
Y = 3 * X
Z = b ** 2
Q = X - Z
external_grad = torch.tensor(1.)
Q.backward(gradient=external_grad)
print(a.grad)
Посмотрите на переменные среды выполнения следующим образом, поскольку Q = X — Z — это вычитание, поэтому соответствующая обратная операция — SubBackward0:
Q = {Tensor} tensor(-28., grad_fn=<SubBackward0>)
X = {Tensor} tensor(8., grad_fn=<PowBackward0>)
Y = {Tensor} tensor(24., grad_fn=<MulBackward0>)
Z = {Tensor} tensor(36., grad_fn=<PowBackward0>)
a = {Tensor} tensor(2., requires_grad=True)
b = {Tensor} tensor(6., requires_grad=True)
Мы можем сравнить его с визуальным представлением DAG. На диаграмме стрелки указывают направление прямого прохода, а узлы представляют обратные функции каждой операции в прямом проходе. Узел синего листа (2) представляет наш тензор листаa
иb
.
На уровне кода в процессе прямого распространения PyTorch не строит явным образом граф расчета обратного распространения, а устанавливает несколько необходимых структур данных, которые можно рассматривать как отношение виртуального графа, но реальной структуры данных графа нет. На прямом проходе каждой итерации для Q = X - Z выполняются следующие операции:
-
1)Введите операцию вычитания: операция вычитания будет отправлена на устройство, где будет построен Q.
-
2)Сначала создайте, как выполнить обратное распространение: при отправке в VariableType сначала создается информация автоградации Q;
-
Создает экземпляр SubBackward0, функции, которая выполняет обратный расчет вычитания.
-
Инициализировать экземпляр SubBackward0
next_edges_
и другие соответствующие члены,next_edges_
Значение члена исходит из входных параметров X и Z прямого прохода.- Если вы войдете
Variable
является листовым узлом, тоnext_edges_
из вводаVariable
изgrad_accumulator_
- Если входная переменная не является конечным узлом, то
next_edges_
из входной переменнойgrad_fn_。
- Используйте новый экземпляр Variable на шаге 3 (то есть результат прямого вычисления Q) для инициализации экземпляра SubBackward0.
input_metadata_
, - Таким образом, мы узнаем, как выполнить обратное распространение Q, но на данный момент мы знаем только, как его вычислить, и это не связано с Q.
- Если вы войдете
-
3)Повторно подключить прямое вычисление и обратное распространение: после прямой операции получается новая переменная, это Q, используйте экземпляр SubBackward0 на шаге 2) для инициализации Q
autograd_meta_->grad_fn_
член. Выполняя обратный расчет для Q, мы знаем, что используя Qautograd_meta_->grad_fn_
элемент для продолжения, то есть SubBackward0 в 2).
Примерно так, как показано ниже:
+-----------------------+ +---------------------------------------+ | Q | | DifferentiableViewMeta | | | | | | autograd_meta_ +---------> | grad_ grad_accumulator_ | | | | | +-----------------------+ | | +----------------------+ grad_fn_ output_nr_ | Q 找到如何计算梯度 | | | | +---------------------------------------+ v +-------------+------------+ +----------------------+ |SubBackward0 | | | | | | Compute the gradient | 如何计算梯度 | apply +---------------> | | | | +----------------------+ | | | | +-----------------------------------------------------+ | next_edges_ +---------> | edge_list | | | | | | other_scalar_type | | [(PowBackward0(self), 0), (PowBackward0(other), 0)] | 输出 | | | | | alpha | +-----------------------------------------------------+ | | | self_scalar_type | +----------------------------------------+ | | | | | input_metadata_ +-----> | [(type of Q, shape of Q, device of Q)] | 输入 | | | | +--------------------------+ +----------------------------------------+
Поскольку при прямом расчете будет создан ряд узлов-примеров на графе расчета, мы сначала проанализируем эти узлы.
-
0x03 Система наследования узлов
Начнем с нижнего узла SubBackward0 на рисунке выше.
3.1 Система наследования
Давайте снова посмотрим на систему наследования SubBackward0.
Определение SubBackward0 находится в: torch/include/torch/csrc/autograd/generated/Functions.h.
struct TORCH_API SubBackward0 : public TraceableFunction {
using TraceableFunction::TraceableFunction;
variable_list apply(variable_list&& grads) override;
std::string name() const override { return "SubBackward0"; }
void release_variables() override {
}
at::ScalarType other_scalar_type;
at::Scalar alpha;
at::ScalarType self_scalar_type;
};
Следовательно, SubBackward0 — это тип Node.
class SubBackward0 : public TraceableFunction
class TraceableFunction : public Node
/// See Node::is_traceable() for definition.
struct TraceableFunction : public Node {
using Node::Node;
bool is_traceable() final {
return true;
}
};
3.2 Node
Мы уже представили Node ранее. Класс Node, представляющий операцию. Каждый узел получает 0 или более переменных и выводит 0 или более переменных. Узлы связаны Edge, что на самом деле осуществляется через переменные-члены Node.next_edges_
связанный. Функции обратного распространения унаследованы от Node.
Мы извлекаем часть кода Node следующим образом:
struct TORCH_API Node : std::enable_shared_from_this<Node> {
/// Performs the `Node`'s actual operation.
virtual variable_list apply(variable_list&& inputs) = 0;
const uint64_t sequence_nr_;
uint64_t topological_nr_ = 0;
// 在前向过程中与该算子相关联的边,对应了前向过程中的输入variable。
edge_list next_edges_;
std::vector<std::unique_ptr<FunctionPreHook>> pre_hooks_;
std::vector<std::unique_ptr<FunctionPostHook>> post_hooks_;
at::SmallVector<InputMetadata, 2> input_metadata_;
// 这里对运算符()进行重载,核心其实就是调用apply()
variable_list operator()(variable_list&& inputs) {
bool pre_sampled = false;
if (at::shouldRunRecordFunction(&pre_sampled)) {
return apply(std::move(inputs));
} else {
return apply(std::move(inputs));
}
}
};
Как видите, apply(variable_list&& inputs) — это чисто виртуальная функция, и ее необходимо реализовать в производном классе. Функция применения является душой функции и основной логикой выполнения вычисления обратного распространения.Функция применения каждого производного класса может быть вызвана через полиморфную функцию C++.
3.3 SubBackward0
Код функции применения SubBackward0 выглядит следующим образом, и вы можете увидеть процесс получения. Код находится в torch/csrc/autograd/generated/Functions.cpp.
variable_list SubBackward0::apply(variable_list&& grads) {
IndexRangeGenerator gen;
auto self_ix = gen.range(1);
auto other_ix = gen.range(1);
variable_list grad_inputs(gen.size());
auto& grad = grads[0];
bool any_grad_defined = any_variable_defined(grads);
if (should_compute_output({ other_ix })) {
// 进行计算
auto grad_result = any_grad_defined ? (handle_r_to_c(other_scalar_type, -grad * alpha.conj())) : Tensor();
copy_range(grad_inputs, other_ix, grad_result); // 拷贝结果到grad_inputs
}
if (should_compute_output({ self_ix })) {
// 进行计算
auto grad_result = any_grad_defined ? (handle_r_to_c(self_scalar_type, grad)) : Tensor();
copy_range(grad_inputs, self_ix, grad_result); // 拷贝结果到grad_inputs
}
return grad_inputs; // 返回grad_inputs
}
Давайте проверим это, просмотрев файл tools/autograd/derivatives.yaml. Вот сопоставление прямого и обратного, которое можно понимать как атомарную операцию, запрашиваемую движком autograd при выполнении вывода обратной цепочки.Мы можем знать, что функции вывода сложения и вычитания используют handle_r_to_c в соответствии со следующим.
- name: add.Tensor(Tensor self, Tensor other, *, Scalar alpha=1) -> Tensor
self: handle_r_to_c(self.scalar_type(), grad)
other: handle_r_to_c(other.scalar_type(), maybe_multiply(grad, alpha.conj()))
result: self_t + maybe_multiply(other_t, alpha)
- name: sub.Tensor(Tensor self, Tensor other, *, Scalar alpha=1) -> Tensor
self: handle_r_to_c(self.scalar_type(), grad)
other: handle_r_to_c(other.scalar_type(), -grad * alpha.conj())
handle_r_to_c определяется следующим образом, что является преобразованием.
Tensor handle_r_to_c(ScalarType self_st, Tensor gradient_result) {
if (!at::isComplexType(self_st) && gradient_result.is_complex()) {
// R -> C
return at::real(gradient_result);
}
return gradient_result;
}
Докажите это кодом:
a = torch.tensor(2., requires_grad=True)
b = torch.tensor(6., requires_grad=True)
Q = a - b
external_grad = torch.tensor(1.)
Q.backward(gradient=external_grad)
这时候运行时如下:
a = {Tensor} tensor(2., requires_grad=True)
T = {Tensor} tensor(2., grad_fn=<PermuteBackward>)
data = {Tensor} tensor(2.)
device = {device} cpu
dtype = {dtype} torch.float32
grad = {Tensor} tensor(1.)
grad_fn = {NoneType} None
b = {Tensor} tensor(6., requires_grad=True)
T = {Tensor} tensor(6., grad_fn=<PermuteBackward>)
data = {Tensor} tensor(6.)
device = {device} cpu
dtype = {dtype} torch.float32
grad = {Tensor} tensor(-1.)
grad_fn = {NoneType} None
Q = {Tensor} tensor(-4., grad_fn=<SubBackward0>)
T = {Tensor} tensor(-4., grad_fn=<PermuteBackward>)
data = {Tensor} tensor(-4.)
device = {device} cpu
dtype = {dtype} torch.float32
grad = {NoneType} None
grad_fn = {SubBackward0} <SubBackward0 object at 0x7fb76e365438>
metadata = {dict: 0} {}
next_functions = {tuple: 2}
0 = {tuple: 2} (<AccumulateGrad object at 0x7fb76e344978>, 0)
1 = {tuple: 2} (<AccumulateGrad object at 0x7fb76e3447b8>, 0)
__len__ = {int} 2
requires_grad = {bool} True
is_cuda = {bool} False
is_leaf = {bool} False
is_meta = {bool} False
is_mkldnn = {bool} False
is_mlc = {bool} False
is_quantized = {bool} False
is_sparse = {bool} False
is_sparse_csr = {bool} False
is_vulkan = {bool} False
is_xpu = {bool} False
layout = {layout} torch.strided
name = {NoneType} None
names = {tuple: 0} ()
ndim = {int} 0
output_nr = {int} 0
requires_grad = {bool} True
shape = {Size: 0} torch.Size([])
3.4 PowBackward0
PowBackward0 определяется следующим образом.
struct TORCH_API PowBackward0 : public TraceableFunction {
using TraceableFunction::TraceableFunction;
variable_list apply(variable_list&& grads) override;
std::string name() const override { return "PowBackward0"; }
void release_variables() override {
std::lock_guard<std::mutex> lock(mutex_);
self_.reset_data();
}
SavedVariable self_;
at::Scalar exponent;
};
variable_list PowBackward0::apply(variable_list&& grads) {
std::lock_guard<std::mutex> lock(mutex_);
IndexRangeGenerator gen;
auto self_ix = gen.range(1);
variable_list grad_inputs(gen.size());
auto& grad = grads[0];
auto self = self_.unpack();
bool any_grad_defined = any_variable_defined(grads);
if (should_compute_output({ self_ix })) {
auto grad_result = any_grad_defined ? (pow_backward(grad, self, exponent)) : Tensor();
copy_range(grad_inputs, self_ix, grad_result);
}
return grad_inputs;
}
Перейдем в tools/autograd/derivatives.yaml и увидим, что используется pow_backward.
- name: pow.Tensor_Scalar(Tensor self, Scalar exponent) -> Tensor
self: pow_backward(grad, self, exponent)
result: auto_element_wise
Наконец, также используется handle_r_to_c.
Tensor pow_backward(Tensor grad, const Tensor & self, const Scalar & exponent) {
if (exponent.equal(0.0)) {
return at::zeros_like(self, LEGACY_CONTIGUOUS_MEMORY_FORMAT);
} else {
auto grad_lambda = [&](auto exp) { return grad * (exp * self.pow(exp - 1)).conj(); };
Tensor out = (exponent.isComplex()) ? grad_lambda(exponent.toComplexDouble()) : grad_lambda(exponent.toDouble());
return handle_r_to_c(self, out);
}
}
3.5 MulBackward0
MulBackward0 определяется следующим образом.
struct TORCH_API MulBackward0 : public TraceableFunction {
using TraceableFunction::TraceableFunction;
variable_list apply(variable_list&& grads) override;
std::string name() const override { return "MulBackward0"; }
void release_variables() override {
std::lock_guard<std::mutex> lock(mutex_);
self_.reset_data();
other_.reset_data();
}
SavedVariable self_;
at::ScalarType other_scalar_type;
at::ScalarType self_scalar_type;
SavedVariable other_;
};
variable_list MulBackward0::apply(variable_list&& grads) {
std::lock_guard<std::mutex> lock(mutex_);
IndexRangeGenerator gen;
auto self_ix = gen.range(1);
auto other_ix = gen.range(1);
variable_list grad_inputs(gen.size());
auto& grad = grads[0];
auto self = self_.unpack();
auto other = other_.unpack();
bool any_grad_defined = any_variable_defined(grads);
if (should_compute_output({ other_ix })) {
auto grad_result = any_grad_defined ? (mul_tensor_backward(grad, self, other_scalar_type)) : Tensor();
copy_range(grad_inputs, other_ix, grad_result);
}
if (should_compute_output({ self_ix })) {
auto grad_result = any_grad_defined ? (mul_tensor_backward(grad, other, self_scalar_type)) : Tensor();
copy_range(grad_inputs, self_ix, grad_result);
}
return grad_inputs;
}
Перейдем в tools/autograd/derivatives.yaml и увидим, что используется mul_tensor_backward.
- name: mul.Tensor(Tensor self, Tensor other) -> Tensor
self: mul_tensor_backward(grad, other, self.scalar_type())
other: mul_tensor_backward(grad, self, other.scalar_type())
result: other_t * self_p + self_t * other_p
Он также использует handle_r_to_c в конце.
Tensor mul_tensor_backward(Tensor grad, Tensor other, ScalarType self_st) {
auto out = grad * other.conj();
return handle_r_to_c(self_st, out);
}
3.6 PermuteBackward
Хотя PermuteBackward не отражен на приведенном выше рисунке, он действительно существует, что является операцией присваивания. PermuteBackward определяется следующим образом:
struct TORCH_API PermuteBackward : public Node {
using Node::Node;
variable_list apply(variable_list&& grads) override;
std::string name() const override { return "PermuteBackward"; }
void release_variables() override {
}
std::vector<int64_t> dims;
};
variable_list PermuteBackward::apply(variable_list&& grads) {
IndexRangeGenerator gen;
auto self_ix = gen.range(1);
variable_list grad_inputs(gen.size());
auto& grad = grads[0];
bool any_grad_defined = any_variable_defined(grads);
if (should_compute_output({ self_ix })) {
auto grad_result = any_grad_defined ? (permute_backwards(grad, dims)) : Tensor();
copy_range(grad_inputs, self_ix, grad_result);
}
return grad_inputs;
}
Перейдем в tools/autograd/derivatives.yaml и увидим, что используется permute_backwards.
- name: permute(Tensor(a) self, int[] dims) -> Tensor(a)
self: permute_backwards(grad, dims)
result: auto_linear
permute_backwards определяется в torch/csrc/autograd/FunctionsManual.cpp.
Tensor permute_backwards(const Tensor & grad, IntArrayRef fwd_dims) {
// invert the permutation
auto ndims = fwd_dims.size();
std::vector<int64_t> dims(ndims);
for(const auto i : c10::irange(ndims)) {
dims[at::maybe_wrap_dim(fwd_dims[i], ndims)] = i;
}
return grad.permute(dims);
}
Давайте подробно разберем форвардный расчет и посмотрим, как он строит зависимости.
0x04 расчет вперед
Из-за нехватки места мы перенесемся прямо в сердце мира C++.
4.1 Реализация вычитания
После послойного распределения наконец вызывается вычитание torch/csrc/autograd/generated/VariableTypeEverything.cpp. PyTorch создаст информацию об автоградации в этой функции. Общая логика такова:
-
1) Операция вычитания будет отправлена на определенное устройство, где будет построена Переменная результата прямого вычисления.
-
2) При диспетчеризации в VariableType будет построена информация об автоградации;
-
Создайте экземпляр SubBackward0, функции обратного вычисления вычитания, с именем экземпляра grad_fn.
-
Устанавливает функцию, используемую при выполнении обратных вычислений.
-
Инициализировать экземпляр SubBackward0
next_edges_
и другие соответствующие члены,next_edges_
Значение члена _ исходит из входного параметра прямого прохода.- Если вы войдете
Variable
является листовым узлом, тоnext_edges_
_ из вводаVariable
изgrad_accumulator_
- Если Variable не является конечным узлом, то
next_edges_
из переменнойgrad_fn_。
- Используйте экземпляр Variable из шага 3 для инициализации экземпляра SubBackward0.
input_metadata_
,
- Если вы войдете
-
3) После прямой операции получается и создается новый результат Variable с использованием Variable::Impl.
-
4) Установите историю вычислений, используйте экземпляр SubBackward0 grad_fn на шаге 2) для инициализации экземпляра переменной
autograd_meta_->grad_fn_
член. -
5) Возврат результата. Результатом здесь является результат прямого вычисления, который в нашем примере равен Q.
Конкретный код выглядит следующим образом:
m.impl("sub.Tensor", TORCH_FN(VariableType::sub_Tensor) ); at::Tensor sub_Tensor(c10::DispatchKeySet ks, const at::Tensor & self, const at::Tensor & other, const at::Scalar & alpha) { auto& self_ = unpack(self, "self", 0); auto& other_ = unpack(other, "other", 1); auto _any_requires_grad = compute_requires_grad( self, other ); (void)_any_requires_grad; auto _any_has_forward_grad_result = isFwGradDefined(self) || isFwGradDefined(other); (void)_any_has_forward_grad_result; std::shared_ptr<SubBackward0> grad_fn; // 构建SubBackward0 if (_any_requires_grad) { // 设置反向计算时候使用的函数 grad_fn = std::shared_ptr<SubBackward0>(new SubBackward0(), deleteNode); // 设置下一条边的所有输入变量 grad_fn->set_next_edges(collect_next_edges( self, other )); // 设置下一条边的类型 grad_fn->other_scalar_type = other.scalar_type(); grad_fn->alpha = alpha; grad_fn->self_scalar_type = self.scalar_type(); } #ifndef NDEBUG c10::optional<Storage> self__storage_saved = self_.has_storage() ? c10::optional<Storage>(self_.storage()) : c10::nullopt; c10::intrusive_ptr<TensorImpl> self__impl_saved; if (self_.defined()) self__impl_saved = self_.getIntrusivePtr(); c10::optional<Storage> other__storage_saved = other_.has_storage() ? c10::optional<Storage>(other_.storage()) : c10::nullopt; c10::intrusive_ptr<TensorImpl> other__impl_saved; if (other_.defined()) other__impl_saved = other_.getIntrusivePtr(); #endif auto _tmp = ([&]() { at::AutoDispatchBelowADInplaceOrView guard; // 前向计算 return at::redispatch::sub(ks & c10::after_autograd_keyset, self_, other_, alpha); })(); // 得到前向计算的输出 auto result = std::move(_tmp); if (grad_fn) { // 将输出variable与grad_fn绑定,grad_fn之中包含了计算梯度的function // 设置计算历史 set_history(flatten_tensor_args( result ), grad_fn); } if (_any_has_forward_grad_result) { auto self_t_raw = toNonOptFwGrad(self); auto self_t = self_t_raw.defined() ? self_t_raw : at::zeros_like(toNonOptTensor(self)); auto other_t_raw = toNonOptFwGrad(other); auto other_t = other_t_raw.defined() ? other_t_raw : at::zeros_like(toNonOptTensor(other)); auto result_new_fw_grad = self_t - maybe_multiply(other_t, alpha); if (result_new_fw_grad.defined()) { // The hardcoded 0 here will need to be updated once we support multiple levels. result._set_fw_grad(result_new_fw_grad, /* level */ 0, /* is_inplace_op */ false); } } return result; }
Мы проанализируем их один за другим. Сначала проанализируйте базовую функцию, а затем вернитесь к анализу sub_Tensor.
-
4.3 Пограничные базисные функции
Сначала мы вводим две функции, которые строят краевые корреляции.
4.3.1 create_gradient_edge
Код create_gradient_edge находится в torch/csrc/autograd/function.h. Его роль:
- Создает «границу» между заданной «переменной» и «функцией», которая является функцией градиента этой переменной (т. е. функцией, которая вычисляет градиент этой переменной во время обратного распространения).
- Эта функция установит свойство «grad_fn» для «переменной».
Метод create_gradient_edge предполагает, что «Переменная» является новым входом в функцию градиента, поэтому его «input_nr» равенfunction->num_inputs()
. Кроме того, это увеличивает количество входов в «узел» на один.
Если вы не хотите увеличивать «num_inputs» «node», используйте «set_gradient_edge» напрямую. Функционально create_gradient_edge примерно эквивалентен variable.set_gradient_edge(функция, функция->add_input_metadata(variable.dispatch_type(), variable.sizes())).
/// Create an `Edge` between the given `variable` and the `function`, which is
/// assumed to be the gradient function of this variable (i.e. the function
/// through which this variable is backpropagated during the backward pass).
/// This sets the `grad_fn` property of the `variable`. This function assumes
/// that the `Variable` is a new input to the gradient function and its
/// `input_nr` thus equal to `function->num_inputs()`. Additionally, it
/// increments the `Node`'s number of inputs by one. Approximately
/// equivalent to `variable.set_gradient_edge(function,
/// function->add_input_metadata(variable.dispatch_type(), variable.sizes()))`.
/// If you don't want the `Node`'s `num_inputs` to be incremented, use
/// `set_gradient_edge` directly.
inline void create_gradient_edge(
Variable& variable,
std::shared_ptr<Node> function) {
// Copy before move.
const auto input_nr = function->add_input_metadata(variable);
impl::set_gradient_edge(variable, {std::move(function), input_nr});
}
4.3.2 set_gradient_edge
Код set_gradient_edge находится в torch/csrc/autograd/variable.cpp.
Наконец, здесь будет вызвана операция настройки истории, это использование края, чтобы действительно настроить, как этот тензор вычисляет градиент, и он настраивается в классе Variable.autograd_meta_
. То есть получить тензорautograd_meta_
, настроить егоgrad_fn_
иoutput_nr_
.
void set_gradient_edge(const Variable& self, Edge edge) {
auto* meta = materialize_autograd_meta(self);
meta->grad_fn_ = std::move(edge.function); // 配置梯度函数
meta->output_nr_ = edge.input_nr; // 配置梯度函数的第几个输出
// For views, make sure this new grad_fn_ is not overwritten unless it is necessary
// in the VariableHooks::grad_fn below.
// This logic is only relevant for custom autograd Functions for which multiple
// operations can happen on a given Tensor before its gradient edge is set when
// exiting the custom Function.
auto diff_view_meta = get_view_autograd_meta(self);
if (diff_view_meta && diff_view_meta->has_bw_view()) {
diff_view_meta->set_attr_version(self._version());
}
}
Среди них код materialize_autograd_meta выглядит следующим образом, и его функция заключается в получении autograd_meta_ от Tensor.
AutogradMeta* materialize_autograd_meta(const Variable& self) {
TORCH_CHECK(self.defined(), "cannot call materialize_autograd_meta() on undefined tensor");
auto p = self.unsafeGetTensorImpl();
if (!p->autograd_meta()) {
p->set_autograd_meta(std::make_unique<AutogradMeta>());
}
return get_autograd_meta(self);
}
Код get_view_autograd_meta выглядит следующим образом, он возвращает DifferentiableViewMeta.
DifferentiableViewMeta* get_view_autograd_meta(const Variable& self) {
// NB: return nullptr if self is not a view
AutogradMeta* meta = get_autograd_meta(self);
if (meta && meta->is_view_) {
return static_cast<DifferentiableViewMeta*>(meta);
} else {
return nullptr;
}
}
4.4 Построение сети
Теперь, когда мы проанализировали SubBackward0 и базовые функции, давайте вернемся назад и проанализируем реализацию sub_Tensor. Во-первых, построить сеть обратного распространения.
- Во-первых, создайте SubBackward0 grad_fn.
- Во-вторых, установите grad_fn, в основном используйте collect_next_edges() для сбора двух переменных подоперации, а затем set_next_edges.
- Затем выполняется прямой расчет для получения результата прямого расчета.
- Наконец, добавьте выходную переменную в историю и привяжите выходную переменную к grad_fn.
Следующий код просто сохраняет ключевую часть sub_Tensor.
std::shared_ptr<SubBackward0> grad_fn;
if (_any_requires_grad) {
// 反向计算时候使用的函数
grad_fn = std::shared_ptr<SubBackward0>(new SubBackward0(), deleteNode);
// 设置下一条边的所有输入变量
grad_fn->set_next_edges(collect_next_edges( self, other ));
grad_fn->other_scalar_type = other.scalar_type();
grad_fn->alpha = alpha;
grad_fn->self_scalar_type = self.scalar_type();
}
auto _tmp = ([&]() {
at::AutoDispatchBelowADInplaceOrView guard;
// 前向计算
return at::redispatch::sub(ks & c10::after_autograd_keyset, self_, other_, alpha);
})();
// 得到前向计算的输出
auto result = std::move(_tmp);
if (grad_fn) {
// 将输出variable与grad_fn绑定,grad_fn之中包含了计算梯度的function
// 将本身计算加入到计算历史之中
set_history(flatten_tensor_args( result ), grad_fn);
}
4.5 Края здания
Ключевой частью построения сети является построение ребер,Вот выходной край, который настраивает обратное распространение (выходной край соответствует двум входам SubBackward0), который состоит из двух шагов:
- Используйте collect_next_edges, чтобы собрать ребро входного параметра (тензора) и получить последующее ребро, которое является градиентом_edge() двух входных параметров self и other.
- Используйте set_next_edges для настройки ребер на тензорах. При вызове set_next_edges инициализируется элемент узла next_edges_ (типа std::vector).
4.5.1 Получение ребер
Функция collect_next_edges используется для получения ребер на основе входных переменных. На самом деле, collect_next_edges — это получение градиентного края себя и другого.
4.5.1.1 gradient_edge
Функция метода gradient_edge состоит в том, чтобы вернуть экземпляр Edge, созданный переменной grad_fn_, Логика следующая:
-
То есть, если узел имеет grad_fn:
- Указывает, что узел является внутренним узлом (созданный внутри операции).
- grad_fn_ — функция градиента этой переменной,
- Затем используйте grad_fn для создания возврата Edge.
-
Если узел не имеет grad_fn:
- Описания являются конечными узлами (созданными пользователем).
- grad_fn_ — аккумулятор градиента этой переменной, которая является экземпляром класса AccumulateGrad (подкласс функции). PyTorch использует grad_accumulator для накопления вывода градиента в эту переменную.
- Используйте grad_accumulator для построения возврата Edge.
Код выглядит следующим образом.Следует отметить, что output_nr является первым выходом текущей переменной в прямом расчете.Для операторов с одним выходом, таких как add или mul, output_nr обычно равен 0, но для операторов с несколькими выходами, таких как split , то output_nr может быть 0,1,2....
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() 这里触发了一个调用,得到了一个SubBackward0实例
if (const auto& gradient = self.grad_fn()) { // 这是一个中间节点,gradient 是一个Function
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的第一个输入
}
}
4.5.1.2 gradient accumulator
Здесь нужно отметить один шаг, то есть в методе gradient_edge есть такой операторreturn Edge(grad_accumulator(self), 0)
, этот код фактически запускает вызов Variable::grad_accumulator().
Когда переменная вызывает этот API в первый раз, генерируется AccumulateGrad для инициализации своего члена grad_accumulator_, код выглядит следующим образом:
std::shared_ptr<Node> grad_accumulator(const Variable& self) {
auto autograd_meta = get_autograd_meta(self);
if (!autograd_meta) {
return nullptr;
}
if (autograd_meta->grad_fn_) {
throw std::logic_error(
"grad_accumulator() should be only called on leaf Variables");
}
if (!autograd_meta->requires_grad_) {
return nullptr;
}
std::lock_guard<std::mutex> lock(autograd_meta->mutex_);
auto result = autograd_meta->grad_accumulator_.lock();
if (result)
return result;
c10::raw::intrusive_ptr::incref(self.unsafeGetTensorImpl());
auto intrusive_from_this = c10::intrusive_ptr<at::TensorImpl>::reclaim(self.unsafeGetTensorImpl());
// 这里会初始化一个AccumulateGrad,配置给grad_accumulator_
result = std::make_shared<AccumulateGrad>(Variable(std::move(intrusive_from_this)));
autograd_meta->grad_accumulator_ = result;
return result;
}
4.5.1.3 AccumulateGrad
Определение AccumulateGrad находится в torch/csrc/autograd/functions/accumulate_grad.h.
struct TORCH_API AccumulateGrad : public Node {
explicit AccumulateGrad(Variable variable_); // 必须用一个Variable构建
variable_list apply(variable_list&& grads) override; // 接收一个list的Variable的实例
Variable variable;
};
Его конструктор находится в torch/csrc/autograd/functions/accumulate_grad.cpp.
Это создаст новый объект AccumulateGrad и будет использовать UINT64_MAX для инициализации функции.sequence_nr_
член.
AccumulateGrad::AccumulateGrad(Variable variable_)
: Node(/*sequence_nr=*/UINT64_MAX),
variable(std::move(variable_)) {
add_input_metadata(variable);
}
4.5.1.4 Сбор ребер
collect_next_edges создает ребра здесь. Все входные ребра собираются.
/// Return the next edges of all the given variables, or tuples of variables.
template <typename... Variables>
edge_list collect_next_edges(Variables&&... variables) {
detail::MakeNextFunctionList make; // 这里将调用gradient_edge
// next_edges_成员的值来自前向时候的输入参数
make.apply(std::forward<Variables>(variables)...);
return std::move(make.next_edges);
}
Определение MakeNextFunctionList выглядит следующим образом: при применении будет построен градиент_edge, что соответствует таким разделам, как градиент_край, упомянутому ранее.
struct MakeNextFunctionList : IterArgs<MakeNextFunctionList> {
edge_list next_edges;
using IterArgs<MakeNextFunctionList>::operator();
void operator()(const Variable& variable) {
if (variable.defined()) {
next_edges.push_back(impl::gradient_edge(variable)); // 调用gradient_edge
} else {
next_edges.emplace_back();
}
}
void operator()(const c10::optional<Variable>& variable) {
if (variable.has_value() && variable->defined()) {
next_edges.push_back(impl::gradient_edge(*variable)); // 调用gradient_edge
} else {
next_edges.emplace_back();
}
}
};
В этот момент получается edge_list, но связи с SubBackward0 нет.
+------------------------+ +----------------------+
| SubBackward0 | | |
| | | Compute the gradient |
| apply +-----------------> | |
| | +----------------------+
| |
| |
| next_edges_ |
| |
| other_scalar_type |
| |
| alpha |
| |
| self_scalar_type |
| |
| input_metadata_ |
| |
+------------------------+
+-----------------------------------------------------+
| edge_list |
| |
| [(MulBackward0(self), 0), (PowBackward0(other), 0)] |
| |
+-----------------------------------------------------+
4.5.2 Настройка краев
После получения всех выходных фронтов следующим шагом будет установка SubBackward0.next_edges_
Выше обязательно обратите внимание,next_edges_
Значение члена исходит из входного параметра во время прямого распространения..
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);
}
}
update_topological_nr установит topological_nr в соответствии с выходным фронтом
void update_topological_nr(const Edge& edge) {
Node* node = edge.function.get();
if (node) {
auto topo_nr = node->topological_nr();
if (topological_nr_ <= topo_nr) {
topological_nr_ = topo_nr + 1;
}
}
}
В сочетании с нашим примером это должно быть так, как показано на рисунке ниже Значение 0 на рисунке ниже следующее: 0 в (PowBackward0(other), 0) означает, что вычисленный вывод SubBackward0 является первым вводом PowBackward0 (исходное возведение в степень имеет только один выход).
+------------------------+ +----------------------+
| SubBackward0 | | |
| | | Compute the gradient |
| apply +-----------------> | |
| | +----------------------+
| |
| | +-----------------------------------------------------+
| next_edges_ +-----------> | edge_list |
| | | |
| other_scalar_type | | [(MulBackward0(self), 0), (PowBackward0(other), 0)] |
| | | |
| alpha | +-----------------------------------------------------+
| |
| self_scalar_type |
| |
| input_metadata_ |
| |
+------------------------+
4.6 История конфигурации
Далее идет история конфигурации, результат — результат прямого распространения, рассчитанный предыдущим кодом,Вот собственно как настроить входные параметры обратного распространения и как рассчитать входные.
if (grad_fn) { // grad_fn 就是 std::shared_ptr<SubBackward0>
// 将输出variable与grad_fn绑定,grad_fn之中包含了计算梯度的function
set_history(flatten_tensor_args( result ), grad_fn);
}
set_history добавит результат прямого распространения в историю, в частности, путем обхода тензоров в результате, а затем добавления каждого тензора в историю. Ключевым моментом является то, что вызывается вышеупомянутый set_gradient_edge,Настройте grad_fn (то есть SubBackward0) на grad_fn_ результата.autograd_meta_.
Вспомните определение переменной-члена grad_fn Tensor.
grad_fn: указывает на объект Function.
- Этот объект Function используется для вычисления градиента ввода во время обратного распространения.
- Если этот тензор не является конечным узлом, Функция является функцией обратного распространения, которая работает в направлении конечного узла, Например, функция, соответствующая узлу O в примере, представляет собой MulBackward, которая является обратной функцией операции умножения ;
После сравнения вы можете узнать, что входной результат прямой операции будет использовать grad_fn_ для вычисления градиента при обратном распространении для вычисления градиента, который здесь является нашим SubBackward0. так чтоУстанавливает, как обратное распространение вычисляет градиенты по отношению к входным данным.
Конкретный код set_history выглядит следующим образом:
inline void set_history(
at::Tensor& variable,
const std::shared_ptr<Node>& grad_fn) {
if (variable.defined()) {
// grad_fn 的 input_metadata 之中添加了输出实例,输出实例在反向传播时候就是输入
auto output_nr = grad_fn->add_input_metadata(variable);
// 输出实例 result 中设置上了grad_fn,这里配置了边,边就是 {grad_fn, output_nr}。
// output_nr_被赋值成了"当前Variable信息在input_metadata_中的index"。
impl::set_gradient_edge(variable, {grad_fn, output_nr});
} else {
// 设置成未定义
grad_fn->add_input_metadata(Node::undefined_input());
}
}
inline void set_history(
std::vector<Variable>&& variables,
const std::shared_ptr<Node>& grad_fn) {
for (auto& variable : variables) {
set_history(variable, grad_fn); // 调用到上面的函数
}
}
4.6.1 Настройка мета
В истории конфигурации первым делом нужно настроить input_metadata.Результат выходного экземпляра добавляется к input_metadata, а результат выходного экземпляра является входом во время обратного распространения.
4.6.1.1 input_metadata_
В классе Node тип input_metadata_ следующий:
at::SmallVector<InputMetadata, 2> input_metadata_;
Конкретные InputMetadata определяются следующим образом:
struct InputMetadata {
InputMetadata(const at::TensorOptions options, at::IntArrayRef shape, at::Device device)
: options_{options}, shape_{shape}, device_{device} {
stream_ = c10::impl::getDeviceGuardImpl(device_.type())->getStream(device_);
}
InputMetadata(const at::Tensor& t)
: InputMetadata(t.options(), t.sizes(), t.device()) { }
private:
const at::TensorOptions options_;
at::DimVector shape_;
at::Device device_ = at::kCPU;
c10::Stream stream_ = c10::Stream(c10::Stream::Default::DEFAULT, device_);
};
4.6.1.2 Настройка мета
В методе add_input_metadata настройте метаданные следующим образом:
/// Adds the type and shape metadata for a new input. Returns the index of
/// of the new input.
uint32_t add_input_metadata (
const at::TensorOptions& options
, at::IntArrayRef shape
, at::Device device) noexcept {
uint32_t input_nr = input_metadata_.size();
input_metadata_.emplace_back(options, shape, device);
return input_nr;
}
После настройки в input_metadata_ добавляется новый InputMetadata, а содержимое InputMetadata является частью информации результата выходной переменной.(type, shape, device)
, индекс в input_metadata_ находится в AutogradMetaoutput_nr_
.
Итак, на данный момент память примерно следующая:
+-------------------------------------------------------------------------------------------------------------+
self +--+ | sub_Tensor |
| | +--------------------------+ +----------------------+ |
+---->+ |SubBackward0 | | | |
| | | | | Compute the gradient | |
other +--+ | +--> grad_fn---> | apply +-----------------> | | |
| | | | +----------------------+ |
| | | | |
| | | | +-----------------------------------------------------+ |
| | | next_edges_ +-----------> | edge_list | |
| | | | | | |
| | | other_scalar_type | | [(PowBackward0(self), 0), (PowBackward0(other), 0)] | |
| | | | | | |
| | | alpha | +-----------------------------------------------------+ |
| | | | |
| | | self_scalar_type | +------------------------------------------------------+ |
| | | | | | |
| | | input_metadata_ +-------> | [(type of result, shape of result, device of result)]| |
| | | | | | |
| | +--------------------------+ +------------------------------------------------------+ |
| | |
| | |
| | +-----------------------+ +---------------------------------------+ |
| | |result | | DifferentiableViewMeta | |
| | | | | | |
| | | autograd_meta_ +-----------> | grad_ grad_accumulator_ | |
| | | | | | |
| | +-----------------------+ | | |
| +--------------------------------------------------------- grad_fn_ output_nr_ | |
| | | |
| +---------------------------------------+ |
+-------------------------------------------------------------------------------------------------------------+
Телефон такой:
4.7 Проверка
Сверяемся с предыдущим примером, продолжаем дорабатывать код примера и получаем:
a = torch.tensor(2., requires_grad=True)
b = torch.tensor(6., requires_grad=True)
X = a ** 3
Y = 3 * X
Z = b ** 2
Q = X - Z
external_grad = torch.tensor(1.)
Q.backward(gradient=external_grad)
print(a.grad)
print(b.grad)
Посмотрите на переменные среды выполнения следующим образом, поскольку Q = X — Z — это вычитание, поэтому соответствующая обратная операция — SubBackward0:
Q = {Tensor} tensor(-28., grad_fn=<SubBackward0>)
X = {Tensor} tensor(8., grad_fn=<PowBackward0>)
Y = {Tensor} tensor(24., grad_fn=<MulBackward0>)
Z = {Tensor} tensor(36., grad_fn=<PowBackward0>)
a = {Tensor} tensor(2., requires_grad=True)
b = {Tensor} tensor(6., requires_grad=True)
Давайте посмотрим на это подробнее.Обратите внимание, что (, 0) 0 здесь означает, что этот узел является 0-м выходом PowBackward0, который является единственным выходом.
Q = {Tensor}
grad_fn = {SubBackward0}
next_functions = {tuple: 2}
0 = {tuple: 2} (<PowBackward0 object at 0x00000177300F4688>, 0)
1 = {tuple: 2} (<PowBackward0 object at 0x00000177300F46C8>, 0)
X = {Tensor}
grad_fn = {PowBackward0}
next_functions = {tuple: 1}
0 = {tuple: 2} (<AccumulateGrad object at 0x00000177300F49C8>, 0)
Z = {Tensor}
grad_fn = {PowBackward0}
next_functions = {tuple: 1}
0 = {tuple: 2} (<AccumulateGrad object at 0x00000177301003C8>, 0)
Y = {Tensor}
grad_fn = {MulBackward0}
next_functions = {tuple: 2}
0 = {tuple: 2} (<PowBackward0 object at 0x0000017730100CC8>, 0)
1 = {tuple: 2} (None, 0)
Соответствующая краткая схема:
Соответствующая логика:
-
- Вызов sub_Tensor с двумя тензорами self и other в качестве аргументов
-
- Используйте grad_fn = std::shared_ptr(new SubBackward0(), deleteNode); для создания SubBackward0. Среди них значение члена next_edges_ grad_fn исходит из входных параметров прямого распространения, таких как self и other.
-
- Используйте at::redispatch::sub для прямого вычисления, чтобы получить результат.
-
-
Используйте set_history для установки истории вычислений. set_history содержит здесь две части
-
Используйте output_nr = grad_fn->add_input_metadata(переменная), чтобы добавить экземпляр вывода в input_metadata grad_fn.
-
Используйте impl::set_gradient_edge(variable, {grad_fn, output_nr}) для атрибутирования результата выходного экземпляра
autograd_meta_->grad_fn_
grad_fn устанавливается в .
-
-
- Наконец, результат возвращается.
Как видите, sub_Tensor настроен для результата следующим образом:
-
Как узнать, как вызвать обратный расчет: результат является результатом прямого вычисления, и есть
autograd_meta_
, который является типом DifferentiableViewMeta, а grad_ и grad_fn_ для DifferentiableViewMeta — это функции градиента, вычисляемые в обратном порядке.grad_fn_ указывает на SubBackward0. - Как рассчитывается обратное распространение: вызвать вычисление SubBackward0.
- Вход для SubBackward0: получен выходной результат прямого вычисления (он будет использоваться как входная переменная при обратном распространении, то есть он установлен в значение SubBackward0.input_metadata_ Above).
-
Выход SubBackward0:Построено
next_edges_
как выходное ребро во время его обратного распространения.
Его логическая схема выглядит следующим образом:
+---------------------------------------------------------------------------------------------------------------+
self +--+ | sub_Tensor +--------------------------+ +----------------------+ |
| | |SubBackward0 | | | |
+---->+ 2 | | | Compute the gradient | |
| 1 | +-----> grad_fn +-----> | apply +-----------------> | | |
other +--+ | | | | +----------------------+ |
| | | | |
| | | | +----------------------+ |
| | | next_edges_ +-----------> | edge_list | |
| | | | | | |
| | | other_scalar_type | | self, other | |
| | | | | | |
| | | alpha | +----------------------+ |
| | | | |
| | | self_scalar_type | |
| | | | |
| | | input_metadata_ +------> [result] |
| | | | ^ |
| | +--------------------------+ | |
| | | 5 |
| | | |
| | 3 result = at::redispatch::sub +--------------------------------------------------------+ |
| | | | | |
| | | + | |
| | | output_nr = grad_fn+>add_input_metadata(variable) | |
| | 4 set_history(result, grad_fn) +-------> | | |
| | | impl::set_gradient_edge(variable,a{grad_fn, output_nr})| |
| | | + | |
| +----------------------------+ | | | |
| 6 | +--------------------------------------------------------+ |
| | | |
| +-----------------------+ | +-----------------------------------+ | 7 |
| |result | | | DifferentiableViewMeta | | |
| | | | | | <---+ |
| | autograd_meta_ +---------------->+ | |
| | | | | grad_ grad_accumulator_ | |
| | | | | | |
| | | +--------+grad_fn_ output_nr_ | |
| | | | | |
| +------------+----------+ +-----------------------------------+ |
| | |
+---------------------------------------------------------------------------------------------------------------+
|
result | 7
v
Телефон такой:
На этом анализ прямого расчета завершен, и мы представим обратное распространение в следующей статье.
0xEE Личная информация
★★★★★★Думая о жизни и технологиях★★★★★★
Публичный аккаунт WeChat:мысли Росси
ссылка 0xFF
Заметки об исследовании Pytorch (тринадцать): анализ базовой реализации обратного процесса
Автоматический механизм вывода pytorch - создание вычислительного графа
How autograd encodes the history
заметки pytorch (граф вычислений + автоград) - узел (1)
Подробно объясните структуру сети в Pytorch.
Динамическая диаграмма PyTorch (ниже)
Динамический график PyTorch (включен)
Вычислительный граф — объяснение примера из PPT Ли Хунъи с Pytorch
Как использовать pytorch для автоматического поиска градиентов
Анализ принципа автоматической деривации PyTorch (Автоград)
Учебники серии Autograd по автоматическому получению pytorch (1)
Разработчики ядра PyTorch лично раскрывают его внутренний механизм