0x00 сводка
Выше было проанализировано, как запустить / принять обратное распространение и как войти в распределенный движок autograd, В этой и следующих статьях будет показано, как работает распределенный движок. Изучив эту статью, читатели смогут понять базовую статическую архитектуру и общую логику выполнения движка dist.autograd.
Другие статьи из этой серии:
[Анализ исходного кода] Распространение PyTorch (1) ------ история и обзор
[Анализ исходного кода] Как PyTorch использует GPU
[Анализ исходного кода] Распределенный PyTorch (2) ----- DataParallel (включен)
[Анализ исходного кода] Распределенный PyTorch (3) ----- DataParallel (ниже)
[Анализ исходного кода] Распределенный PyTorch (7) ----- Группа процессов DistributedDataParallel
[Анализ исходного кода] Распределенный PyTorch (8) -------- Бумага DistributedDataParallel
[Анализ исходного кода] Распределенный PyTorch (9) ----- Инициализация DistributedDataParallel
[Анализ исходного кода] PyTorch, распространяемый Autograd (1) ---- дизайн
[Анализ исходного кода] PyTorch, распространяемый Autograd (2) ---- Фонд RPC
[Анализ исходного кода] PyTorch, распространяемый Autograd (3) ---- контекстно-зависимый
[Анализ исходного кода] PyTorch распространяет Автоград (4) ---- как врезаться в движок
Для лучшего объяснения код в этой статье будет соответственно упрощен в соответствии с конкретной ситуацией.
0x01 Система поддержки
Начнем с рассмотрения некоторых внутренних систем крепления двигателя.
1.1 Вход в двигатель
Запись движка вызывается в обратной функции, и она поступает в движок из DistEngine::getInstance().execute Как видно из предыдущей статьи, это активный движок вызова.
void backward(
int64_t context_id,
const variable_list& roots,
bool retain_graph) {
RECORD_FUNCTION(
kDistAutogradBackwardProfilingKey, std::vector<c10::IValue>());
try {
DistEngine::getInstance().execute(context_id, roots, retain_graph);
} catch (std::exception& e) {
throw std::runtime_error(e.what());
}
}
1.2 SendRpcBackward
Движок пассивного вызова запускается с SendRpcBackward.. SendRpcBackward — это оператор обратного распространения, соответствующий поведению отправки при прямом распространении. DistAutogradContext хранит информацию о каждом распределенном автоградации на рабочем потоке, инкапсулирует прямое и обратное распространение в распределенном автоградации и накапливает градиенты, что позволяет избежать влияния нескольких рабочих процессов на градиенты друг друга. В контексте DistAutogradContext есть переменная-член, которая записывает оператор обратного распространения, соответствующий всем отправляющим поведениям этого работника.
std::unordered_map<int64_t, std::shared_ptr<SendRpcBackward>> sendAutogradFunctions_;
Все в sendAutogradFunctions_ — это SendRpcBackward.
1.2.1 Анатомия
SendRpcBackward В рамках распределенной реализации автоградации мы добавляем функцию автоградации «SendRpcBackward» в граф автоградации всякий раз, когда мы отправляем RPC с одного узла на другой. Это функция-заполнитель для запуска механизма автоградации текущего рабочего процесса при обратном распространении. Ребра этой функции autograd являются входными данными для метода RPC.
Во время обратного распространения эта функция будет поставлена в очередь для выполнения в механизме автоградации, который в конечном итоге запустит остальную часть графа автоградации.
SendRpcBackward фактически является корнем графа автоградации на локальном узле. Приведем предыдущую схему следующим образом:
- SendRpcBackward не получает никаких «входных данных», вместо этого инфраструктура RPC передает этой функции градиенты, чтобы инициировать локальные вычисления автоградации.
- Сторона ввода SendRpcBackward — это ввод метода RPC, который представляет собой градиент.
1.2.2 Определения
SendRpcBackward является производным классом от Node. Поскольку это Node, он имеет next_edges. Вы можете видеть, что его новая переменная-член — grads_.
// As part of our distributed autograd implementation, whenever we send an RPC
// from one node to another, we add a 'SendRpcBackward' autograd function to the
// autograd graph. This is more or less a placeholder function that is used to
// kickoff the autograd engine on the current worker on the backward pass. The
// edges for this autograd function are the inputs to the RPC method.
//
// During the backward pass, this function is queued for execution in the
// autograd engine which eventually runs the rest of the autograd graph.
struct TORCH_API SendRpcBackward : public torch::autograd::Node {
public:
torch::autograd::variable_list apply(
torch::autograd::variable_list&& inputs) override;
// SendRpcBackward is actually the root of an autograd graph on the local
// node. As a result, it doesn't receive any 'inputs', but rather the RPC
// framework passes gradients over to this function to kickoff local autograd
// computation.
void setGrads(const torch::autograd::variable_list& grads);
// Retrieve the grads for the function.
const torch::autograd::variable_list& getGrads() const;
private:
torch::autograd::variable_list grads_;
};
1.2.3 Сборка
В процессе прямого распространения addSendRpcBackward создаст SendRpcBackward и установит его входное ребро прямого распространения в качестве выходного края обратного распространения в SendRpcBackward.
void addSendRpcBackward(
const ContextPtr& autogradContext,
const AutogradMetadata& autogradMetadata,
std::vector<torch::Tensor>& tensors) {
// Attach autograd information only for tensors requiring grad.
std::vector<torch::Tensor> tensors_with_grad;
std::copy_if(
tensors.begin(),
tensors.end(),
std::back_inserter(tensors_with_grad),
[](const torch::Tensor& t) { return t.requires_grad(); });
// Attach the appropriate autograd edges.
auto grad_fn = std::make_shared<SendRpcBackward>(); // 构建了 SendRpcBackward
grad_fn->set_next_edges(
torch::autograd::collect_next_edges(tensors_with_grad));
// Add the appropriate input metadata for the grad_fn.
for (const auto& tensor : tensors_with_grad) {
grad_fn->add_input_metadata(tensor); // 添加边 SendRpcBackward
}
// Record the send autograd function in our current context.
// 插入到上下文
autogradContext->addSendFunction(grad_fn, autogradMetadata.autogradMessageId);
}
1.2.4 grads_
Как было показано ранее, новая переменная-член SendRpcBackward имеет видgrads_
, Посмотримgrads_
Как настроить и использовать?
SendRpcBackward обеспечивает операции установки и получения.
void SendRpcBackward::setGrads(const torch::autograd::variable_list& grads) {
grads_ = grads;
}
const torch::autograd::variable_list& SendRpcBackward::getGrads() const {
return grads_;
}
Когда он будет использоваться? В torch/csrc/distributed/rpc/request_callback_no_python.cpp есть processBackwardAutogradReq. processBackwardAutogradReq будет:
- Используйте sendFunction->setGrads(gradientsCall.getGrads()) для установки градиентов, переданных с удаленного конца.
- Вызовите DistEngine::getInstance().executeSendFunctionAsync, чтобы запустить обработчик для запуска локального обратного вычисления.
Соответствует следующему тексту в конструкции, являющейся отправной точкой пассивного входа в движок:
SendRpcBackward фактически является корнем графа автоградации на локальном узле. Таким образом, он не получает никаких «входных данных», вместо этого инфраструктура RPC передает градиент этой функции, чтобы начать локальное вычисление автоградации.
Конкретный код выглядит следующим образом:
void RequestCallbackNoPython::processBackwardAutogradReq(
RpcCommandBase& rpc,
const int64_t messageId,
const c10::intrusive_ptr<JitFuture>& responseFuture) const {
auto& gradientsCall = static_cast<PropagateGradientsReq&>(rpc);
const auto& autogradMetadata = gradientsCall.getAutogradMetadata();
// Retrieve the appropriate autograd context.
auto autogradContext = DistAutogradContainer::getInstance().retrieveContext(
autogradMetadata.autogradContextId);
// Lookup the appropriate 'send' function to enqueue.
std::shared_ptr<SendRpcBackward> sendFunction =
autogradContext->retrieveSendFunction(autogradMetadata.autogradMessageId);
// Attach the gradients to the send function.
sendFunction->setGrads(gradientsCall.getGrads()); // 这里设置,就是把RPC传来的梯度赋值
// Now execute the autograd graph using the "distributed engine."
auto execFuture = DistEngine::getInstance().executeSendFunctionAsync( // 这里使用了 grads_
autogradContext, sendFunction, gradientsCall.retainGraph());
// Our response is satisfied when the rpcs come back.
execFuture->addCallback([responseFuture, messageId](JitFuture& execFuture) {
if (!execFuture.hasError()) {
Message m = std::move(PropagateGradientsResp()).toMessage();
m.setId(messageId);
responseFuture->markCompleted(
IValue(c10::make_intrusive<Message>(std::move(m))));
} else {
responseFuture->setError(execFuture.exception_ptr());
}
});
}
executeSendFunctionAsync будет использовать sendFunction->getGrads() для извлечения градиентов и работы.
c10::intrusive_ptr<c10::ivalue::Future> DistEngine::executeSendFunctionAsync(
const ContextPtr& autogradContext,
const std::shared_ptr<SendRpcBackward>& sendFunction,
bool retainGraph) {
// Typically the local autograd engine ensures stream synchronizations between
// nodes in the graph. However, for distributed autograd the sendFunction
// inputs might have been retrieved over the wire on a separate stream and the
// sendFunction itself runs on a different stream. As a result, we need to
// manually synchronize those two streams here.
const auto& send_backward_stream = sendFunction->stream(c10::DeviceType::CUDA);
if (send_backward_stream) {
for (const auto& grad : sendFunction->getGrads()) { // 这里有获取
const auto guard = c10::impl::VirtualGuardImpl{c10::DeviceType::CUDA};
const auto default_stream = guard.getStream(grad.device());
if (send_backward_stream != default_stream) {
auto event = c10::Event{c10::DeviceType::CUDA};
event.record(default_stream);
send_backward_stream->wait(event);
}
}
}
// 省略后续代码
Подробности следующие:
0x02 Определение
2.1 Определения
Определение DistEngine выглядит следующим образом.Для лучшего понимания, часть кода удалена ниже:
class TORCH_API DistEngine {
public:
// Retrieve the singleton instance.
static DistEngine& getInstance();
// Given a list of root variables, start the distributed backwards pass from
// these variables and accumulate all the gradients in the current autograd
// context on each node. This method is used to kickoff distributed autograd
// on a single node.
void execute(
int64_t context_id,
const torch::autograd::variable_list& roots,
bool retainGraph);
// Given a send function to execute in the autograd engine, ensures we compute
// dependencies once for this node and enqueues the send function for execute
// in the engine.
// This method is used to kick off the autograd computation on a node when it
// receives gradients from the corresponding 'recv' method on another node.
// The gradients are accumulated in the provided autograd context.
c10::intrusive_ptr<c10::ivalue::Future> executeSendFunctionAsync(
const ContextPtr& autogradContext,
const std::shared_ptr<SendRpcBackward>& sendFunction,
bool retainGraph);
// Number of backward passes currently running for the Distributed Engine.
size_t numBackwardPasses() const;
// Returns key-value pairs consisting of useful debugging information related
// to distributed autograd.
std::unordered_map<std::string, int> getDebugInfo() const;
// Validates the input roots for the backward computations and retrieves the
// appropriate root edges and corresponding gradients. Populates root_edges
// with the appropriate gradient edges and grads with the gradients for each
// edge.
void validateRootsAndRetrieveEdges(
const torch::autograd::variable_list& roots,
torch::autograd::edge_list& rootEdges,
torch::autograd::variable_list& grads);
// Given the autograd context, root edges and grads, we compute dependencies
// for the local node and fill out the provided GraphTask and GraphRoot with
// appropriate information for the local autograd engine.
// We also determine all leaf nodes(functions) in the graph and accumulate
// them in outputEdges.
void computeDependencies(
const ContextPtr& context,
const torch::autograd::edge_list& rootEdges,
const torch::autograd::variable_list& grads,
const std::shared_ptr<torch::autograd::Node>& graphRoot,
torch::autograd::edge_list& outputEdges,
bool retainGraph);
// Given a pre-populated GraphTask and a root node, compute the backward pass
// for the autograd graph until the graph task ready queue is empty.
//
// This method assumes that the appropriate GraphTask has already been
// initialized appropriately. It will construct a local ready queue to
// traverse the GraphTask instead of using the GraphTask embedded
// cpu_ready_queue, this is because dist engine might run the same GraphTask
// from different SendFunctions concurrently in different threads. The method
// will only mark the GraphTask as completed when it needes to, which means it
// might not mark as completed for every call as dist engine would like to
// keep the GraphTask alive when it not receives all gradients.
//
// When `incrementOutstandingTasks=false`, the function does not increment
// 'outstanding_tasks_' in the appropriate GraphTask. It is assumed we've
// already done this before hand for this task (to ensure we don't pre-mark
// this graph_task as completed). This is useful in the distributed autograd
// case where we need to increment 'outstanding_tasks_' first to indicate the
// local autograd engine the graph task is not completed until it receives the
// signals from other workers over the network.
//
// XXX: calling this function assumes that we will have NO GPU nodetasks be
// executed for the graph_task, the caller of this function need to ensure
// this otherwise there will be undefined behaviors. A correct way to fix this
// is to re-design the autograd engine so that GPU worker thread to behave the
// same as CPU caller thread, record the operation/thread for the device, and
// reuse it in backward.
// TODO: 1. Add assert in the dist engine to ensure no GPU NodeTasks during
// backward
// 2. properly setup the thread local ready queue to enable reentrant
// backwards
void execute_graph_task_until_ready_queue_empty(
torch::autograd::NodeTask&& node_task,
bool incrementOutstandingTasks = true);
// Run the local autograd engine using the provided graphTask and graphRoot
// and accumulate the gradients part 'outputEdges' in the provided autograd
// context.
c10::intrusive_ptr<c10::ivalue::Future> runEngineAndAccumulateGradients(
const ContextPtr& autogradContext,
const std::shared_ptr<torch::autograd::Node>& graphRoot,
const torch::autograd::edge_list& outputEdges,
bool incrementOutStandingTasks = true);
// Run after the backward pass is done to appropriately cleanup structures.
void cleanupBackwardPass(const ContextPtr& autogradContext);
// Global thread to execute CPU continuations.
void globalCpuThread(
const std::shared_ptr<torch::autograd::ReadyQueue>& ready_queue);
// Set of autograd context_ids, which we have already initialized for
// distributed autograd on this node (e.g.: already computed dependencies)
std::unordered_set<int64_t> initializedContextIds_;
mutable std::mutex initializedContextIdsLock_;
// Reference to local autograd engine.
torch::autograd::Engine& engine_;
// Ready queue used by the CPU thread in distributed engine.
// See Note [GPU to CPU continuations]
// 每个 GraphTask都把 global_cpu_ready_queue_ 设置为自己的 cpu_ready_queue_
std::shared_ptr<torch::autograd::ReadyQueue> global_cpu_ready_queue_;
// See Note [GPU to CPU continuations]
std::thread global_cpu_thread_;
friend class BackwardPassCleanupGuard;
};
2.2 Синглтон
Движок использует шаблон синглтона, так что только один синглтон работает на каждого работника.
DistEngine& DistEngine::getInstance() {
// Leaky singleton to avoid module destructor race.
static DistEngine* engine = new DistEngine();
return *engine;
}
2.3 Важные примечания
В исходном коде PyTorch есть много подробных комментариев, давайте выберем несколько и посмотрим.
2.3.1 Переменные-члены
Две глобальные переменные-члены, связанные с процессором, определены в коде следующим образом, обе из которых указывают на то, что вам нужно увидеть примечание [продолжения от GPU к CPU].
// Ready queue used by the CPU thread in distributed engine.
// See Note [GPU to CPU continuations]
std::shared_ptr<torch::autograd::ReadyQueue> global_cpu_ready_queue_;
// See Note [GPU to CPU continuations]
std::thread global_cpu_thread_;
2.3.2 Сборка
Конкретное место инициализации этих двух переменных-членов находится в конструкторе.
DistEngine::DistEngine()
: initializedContextIds_(),
engine_(Engine::get_default_engine()),
global_cpu_ready_queue_(std::make_shared<ReadyQueue>()), // 这里构建了
global_cpu_thread_( // 这里构建了
&DistEngine::globalCpuThread,
this,
global_cpu_ready_queue_) {
// Note [GPU to CPU continuations]
// ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Initialize a single CPU thread to execute continuations from GPU
// tasks. The multithreaded structure for the distributed engine works
// well only for CPU tasks. If we have an order of tasks like
// CPU->GPU->CPU, distributed autograd has no thread to execute the last
// CPU task on. To fix this, we introduce a global CPU thread to handle
// such situations and it will be responsible for executing these CPU
// tasks. The CPU thread has its own ready_queue which is used as the
// cpu_ready_queue for all GraphTasks for DistEngine. This ensures all GPU
// to CPU continuations are enqueued on this thread. The global CPU thread
// simply dequeues tasks from the global queue and calls
// "execute_graph_task_until_ready_queue_empty" on a JIT thread to execute the
// appropriate task.
global_cpu_thread_.detach(); // detach之后就独立运行了
}
2.3.3 GPU to CPU continuations
Ниже приведен перевод и понимание продолжения GPU в CPU.
Сначала с продолжением нужно было связываться на языке схемы, и я видел, как это использовалось во многих языках позже.Я не нашел хорошей концепции продолжения для этой концепции и временно использую перевод «продолжение».
Для выполнения продолжений задач графического процессора необходимо инициализировать отдельный поток ЦП для обработки. Многопоточная структура распределенного движка подходит только для задач ЦП. Если у нас есть порядок задач CPU->GPU->CPU, распределенный автоград не имеет потока для выполнения последней задачи CPU. Чтобы решить эту проблему, мы вводим глобальный поток ЦП для обработки этой ситуации, который будет отвечать за выполнение этих задач ЦП.
Поток ЦП имеет свою собственную очередь готовности (ready_queue), которая используется в качестве очереди готовности ЦП (cpu_ready_queue) для всех GraphTasks DistEngine. Это гарантирует, что все продолжения GPU-to-CPU будут поставлены в очередь в этом потоке. Глобальный поток ЦП просто берет задачу из глобальной очереди и вызывает "execute_graph_task_until_ready_queue_empty" в потоке JIT для выполнения соответствующей задачи.
If we have an order of tasks like CPU->GPU->CPU, distributed autograd has no thread to execute the last CPU task on. To fix this, we introduce a global CPU thread to handle such situations and it will be responsible for executing these CPU tasks. The CPU thread has its own ready_queue which is used as the cpu_ready_queue for all GraphTasks for DistEngine. This ensures all GPU to CPU continuations are enqueued on this thread. The global CPU thread simply dequeues tasks from the global queue and calls "execute_graph_task_until_ready_queue_empty" on a JIT thread to execute the appropriate task.
2.3.4 Разрушение
Среди деструкторов есть следующие, которые выполняют связанные операции с этими двумя переменными-членами для завершения работы движка.
DistEngine::~DistEngine() {
// Ensure we shutdown the CPU thread.
TORCH_ASSERT_NO_GIL_WITHOUT_PYTHON_DEP();
global_cpu_ready_queue_->pushShutdownTask();
global_cpu_thread_.join();
}
2.3.5 Вставить очередь
Куда вставить в global_cpu_ready_queue_? Будут вставки в DistEngine::computeDependencies. Во-первых, каждая задача GraphTask устанавливает для global_cpu_ready_queue_ значение cpu_ready_queue. В конструктор GraphTask передается параметр global_cpu_ready_queue_ при его вызове.
void DistEngine::computeDependencies(
const ContextPtr& autogradContext,
const edge_list& rootEdges,
const variable_list& grads,
const std::shared_ptr<Node>& graphRoot,
edge_list& outputEdges,
bool retainGraph) {
// Build the graph task and graph root.
// NOTE: we don't need to build and pass a cpu_ready_queue to GraphTask
// as we use execute_graph_task_until_ready_queue_empty, which will build
// a separate ReadyQueue for each call.
auto graphTask = std::make_shared<GraphTask>(
/* keep_graph */ retainGraph,
/* create_graph */ false,
/* depth */ 0,
/* cpu_ready_queue */ global_cpu_ready_queue_,
/* exit_on_error */ true);
// 省略其他 graphTask 初始化
// Let autograd context take ownership of the GraphTask.
// 上下文里面设置了 GraphTask
autogradContext->setGraphTask(std::move(graphTask));
}
Поэтому, если GraphTask наконец возвращается, когда ЦП требуется для работы, это используется единообразно.
2.3.6 Рабочие потоки
globalCpuThread — это рабочий поток, который извлекает NodeTask из очереди готовности и выполняет его.
void DistEngine::globalCpuThread(
const std::shared_ptr<ReadyQueue>& ready_queue) {
while (true) {
NodeTask task = ready_queue->pop();
if (task.isShutdownTask_) {
// Need to shutdown this thread.
C10_LOG_API_USAGE_ONCE("torch.autograd.thread_shutdown");
break;
}
auto graphTask = task.base_.lock();
if (graphTask == nullptr) {
// GraphTask has expired, ignore and continue processing.
continue;
}
// Launch the execution on a JIT thread.
at::launch([this,
graphTask,
graphRoot = task.fn_,
variables =
InputBuffer::variables(std::move(task.inputs_))]() mutable {
InputBuffer inputs(variables.size());
for (size_t i = 0; i < variables.size(); i++) {
inputs.add(i, std::move(variables[i]), c10::nullopt, c10::nullopt);
}
execute_graph_task_until_ready_queue_empty(
/*node_task*/ NodeTask(graphTask, graphRoot, std::move(inputs)),
/*incrementOutstandingTasks*/ false);
});
}
}
0x03 Общее выполнение
Общее выполнение выполняется в DistEngine::execute, которое разделено на следующие шаги:
- Используйте contextId для получения прямого контекста.
- Используйте validateRootsAndRetrieveEdges для проверки.
- Создайте GraphRoot и используйте его для управления обратным распространением, которое можно рассматривать как виртуальный корень.
- Используйте ComputeDependencies для вычисления зависимостей.
- Используйте runEngineAndAccumulateGradients для вычислений обратного распространения.
- Используйте clearAndWaitForOutstandingRpcsAsync, чтобы дождаться завершения RPC.
Видно, что по сравнению с обычными движками присутствует дополнительный процесс вычисления корневого ребра и генерации информации о градиенте на ребре. Поскольку в обычном процессе прямого распространения они уже настроены, но в распределенных вычислениях прямое распространение не вычисляет их, поэтому его необходимо вычислить перед обратным распространением.
void DistEngine::execute(
int64_t contextId,
const variable_list& roots,
bool retainGraph) {
// Retrieve the context for the given context_id. This will throw if the
// context_id is invalid.
auto autogradContext =
DistAutogradContainer::getInstance().retrieveContext(contextId);
// Perform initial pre-processing.
edge_list rootEdges;
variable_list grads;
validateRootsAndRetrieveEdges(roots, rootEdges, grads);
// 构造一个GraphRoot,用它来驱动后向传播,可以认为是一个虚拟根
std::shared_ptr<Node> graphRoot =
std::make_shared<GraphRoot>(rootEdges, grads);
edge_list outputEdges;
// Compute dependencies locally, starting from all roots and all 'send'
// functions.
{
std::lock_guard<std::mutex> guard(initializedContextIdsLock_);
// Context should not have been initialized already.
TORCH_INTERNAL_ASSERT(
initializedContextIds_.find(autogradContext->contextId()) ==
initializedContextIds_.end());
// 计算依赖
computeDependencies(
autogradContext, rootEdges, grads, graphRoot, outputEdges, retainGraph);
// Mark the autograd context id as initialized.
initializedContextIds_.insert(autogradContext->contextId());
}
BackwardPassCleanupGuard guard(autogradContext);
// This needs to be blocking and as a result we wait for the future to
// complete.
runEngineAndAccumulateGradients(autogradContext, graphRoot, outputEdges)
->waitAndThrow(); // 反向传播计算
// Wait for all of the outstanding rpcs to complete.
autogradContext->clearAndWaitForOutstandingRpcsAsync()->waitAndThrow();
}
0x04 Проверка узлов и ребер
Давайте посмотрим, как выполнить проверку дальше.
validateRootsAndRetrieveEdges используется для проверки достоверности узлов и ребер Конкретная логика такова:
- Проверьте допустимость корневого узла и получите ребра корневого узла.
- Посмотрите, не пуст ли корневой узел.
- Нужно ли корневому узлу вычислять градиент.
- Имеет ли корневой узел функцию градиента.
- Вычислите края градиента и сгенерируйте соответствующий градиент.
- Вызовите validate_outputs для проверки выходных данных.
void DistEngine::validateRootsAndRetrieveEdges(
const variable_list& roots,
edge_list& rootEdges,
variable_list& grads) {
TORCH_CHECK(!roots.empty(), "No tensors provided for gradient computation.");
TORCH_INTERNAL_ASSERT(rootEdges.empty());
TORCH_INTERNAL_ASSERT(grads.empty());
// Verify roots are all scalar and require gradients.
for (const auto& root : roots) {
TORCH_CHECK(root.requires_grad(), "requires_grad not set on root");
TORCH_CHECK(
root.numel() == 1, // python numel()函数:返回数组中元素的个数
root.name(),
" is not a scalar, all roots need to be scalar");
TORCH_CHECK(
root.grad_fn(),
root.name(),
" does not have a valid gradient function.");
// Compute the root edges and generate the appropriate gradients.
rootEdges.push_back(torch::autograd::impl::gradient_edge(root));
grads.push_back(at::ones_like(root, LEGACY_CONTIGUOUS_MEMORY_FORMAT));
}
// Validate rootEdges and grads.
validate_outputs(
rootEdges, grads, [](const std::string& msg) { return msg; });
}
4.1 gradient_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() 这里触发了一个调用,得到了一个Node实例
if (const auto& gradient = self.grad_fn()) {
return Edge(gradient, self.output_nr()); // self.output_nr() 表示本Edge是function的第n个输入。前向传播时候的第 n 个输出在反向传播时候就是第 n 个输入。
} else {
return Edge(grad_accumulator(self), 0); // 0表示本Edge是function的第一个输入
}
}
4.2 validate_outputs
Он определен в torch/csrc/autograd/engine.cpp и вызывается как родными, так и распределенными движками. В validate_outputs много кода проверки.
- Выход, если количество градиентов отличается от количества ребер.
- Перебрать градиенты для каждого градиента:
- Получите соответствующее ребро, если ребро недействительно, перейдите к следующему градиенту.
- Используйте input_metadata для получения входной информации.
- Если градиент не определен, также перейдите к следующему градиенту.
- Выйти, если размер градиента отличается от входной формы.
- Ряд суждений делается об оборудовании градиента и оборудовании метаданных.
Конкретный код выглядит следующим образом:
void validate_outputs(
const edge_list& edges,
variable_list& grads,
const std::function<std::string(const std::string&)>& format_error) {
if (grads.size() != edges.size()) {
std::stringstream ss;
ss << "invalid number of gradients - expected ";
ss << edges.size() << ", but got " << grads.size();
AT_ERROR(format_error(ss.str()));
}
for (size_t i = 0; i < grads.size(); i++) {
const auto& edge = edges[i];
if (!edge.is_valid()) continue;
const auto& metadata = edge.function->input_metadata(edge.input_nr);
auto& grad = grads[i];
if (!grad.defined()) {
// FIXME: TestJit.test_ge_optimized fails this assertion.
// std::stringstream ss;
// ss << "undefined gradient at index " << i;
// AT_ERROR(format_error(ss.str()));
continue;
}
// 如果梯度尺寸与输入形状不同,则退出
if (!grad.sizes().equals(metadata.shape())) {
if (!at::is_expandable_to(metadata.shape(), grad.sizes())) {
std::stringstream ss;
ss << "invalid gradient at index " << i << " - got ";
ss << grad.sizes() << " but expected shape compatible with ";
ss << metadata.shape();
AT_ERROR(format_error(ss.str()));
}
grad = at::sum_to(std::move(grad), metadata.shape());
}
bool input_is_complex = isComplexType(c10::typeMetaToScalarType(metadata.options().dtype()));
bool grad_is_complex = isComplexType(grad.scalar_type());
TORCH_CHECK(isFloatingType(grad.scalar_type()) || (input_is_complex == grad_is_complex));
if (c10::typeMetaToScalarType(metadata.options().dtype()) != grad.scalar_type()) {
grad = grad.to(c10::typeMetaToScalarType(metadata.options().dtype()));
}
if (grad.device() != metadata.device() &&
grad.dim() == 0) {
grad = grad.to(metadata.device());
}
if (!is_compatible_type(metadata.options(), grad.options())) {
std::stringstream ss;
ss << "invalid gradient at index " << i << " - expected type ";
ss << metadata.options() << " but got " << grad.options();
AT_ERROR(format_error(ss.str()));
}
auto grad_device = grad.device();
if (grad_device != metadata.device()) {
std::stringstream ss;
ss << "invalid gradient at index " << i << " - expected device ";
ss << metadata.device() << " but got " << grad_device;
AT_ERROR(format_error(ss.str()));
}
// We should not build graph for Tensors that are not differentiable
TORCH_INTERNAL_ASSERT(isDifferentiableType(grad.scalar_type()));
}
}
4.3 VS нормальный двигатель
Давайте сравним проверочную часть с обычным двигателем.
В обычном движке вызывается только validate_outputs.
auto Engine::execute(const edge_list& roots,
const variable_list& inputs,
bool keep_graph,
bool create_graph,
bool accumulate_grad,
const edge_list& outputs) -> variable_list {
validate_outputs(roots, const_cast<variable_list&>(inputs), [](const std::string& msg) {
return msg;
});
// 省略其他后续代码
Таким образом, для части проверки DistEngine можно резюмировать следующим образом:
- Проверьте.
- Вычислите ребро, соответствующее корню, и сгенерируйте соответствующий градиент в соответствии с корнями.
- Затем используйте validate_outputs для проверки вывода.
0x05 Вычислительные зависимости
Вспомним алгоритм режима FAST из проектной документации. Ключевое предположение этого алгоритма состоит в том, что когда мы запускаем обратное распространение, каждыйsend
Функция имеет зависимость 1. Другими словами, мы предполагаем, что будем получать градиенты через RPC от другого узла. Алгоритм следующий:
- Начнем с воркеров с обратным распространением корней (все корни должны быть локальными).
- Найти весь текущий распределенный контекст Autograd.
send
функция . - из предоставленного корня и все, что мы получили
send
Функция запускается, и мы вычисляем зависимости локально. - После вычисления зависимостей используйте предоставленный корень для запуска локального механизма автоградации.
- Когда движок автограда выполняет
recv
функция,recv
Функция отправляет входной градиент соответствующему воркеру через RPC. каждыйrecv
Функция знает идентификатор целевого рабочего, поскольку он записывается как часть прямого прохода. пройти черезautograd_context_id
иautograd_message_id
Долженrecv
Функция отправляется на удаленный хост. - Когда удаленный хост получает этот запрос, мы используем
autograd_context_id
иautograd_message_id
найти подходящееsend
функция. - Если это первый раз, когда работник получил
autograd_context_id
запросы, он будет вычислять зависимости локально, как описано в пунктах 1-3 выше. - то получит в точке 6
send
Методы вставляются в очередь для выполнения на локальном механизме автоградации этого рабочего. - Наконец, мы не в Тензоре.
.grad
вместо того, чтобы накапливать градиенты поверх каждого распределенного контекста Autograd отдельно. Градиенты хранятся вDict[Tensor, Tensor]
среди ,Dict[Tensor, Tensor]
По сути, это карта от Tensor к связанным с ними градиентам, и эту карту можно получить с помощью API get_gradients().
Эта глава соответствует первым трем пунктам алгоритма.Эта часть является одним из самых больших отличий от обычных двигателей..
5.1 Общий процесс
Расчетные зависимости разделены на две части, первая часть - выполнение подготовительных работ, вторая часть - расчет зависимостей, а третья часть - получение, какие функции необходимо рассчитать по зависимостям.
Сначала приведем общий код и комментарии, а подробно разберем их позже.
void DistEngine::computeDependencies(
const ContextPtr& autogradContext,
const edge_list& rootEdges,
const variable_list& grads,
const std::shared_ptr<Node>& graphRoot,
edge_list& outputEdges,
bool retainGraph) {
TORCH_INTERNAL_ASSERT(graphRoot, "graphRoot is null!");
// 第一部分,准备工作
// 1. 生成一个GraphTask
// Build the graph task and graph root.
// NOTE: we don't need to build and pass a cpu_ready_queue to GraphTask
// as we use execute_graph_task_until_ready_queue_empty, which will build
// a separate ReadyQueue for each call.
// 不需要给 GraphTask 传一个cpu_ready_queue,因为我们后面使用execute_graph_task_until_ready_queue_empty,在那里会给每一个调用建立一个独立的ReadyQueue
auto graphTask = std::make_shared<GraphTask>(
/* keep_graph */ retainGraph,
/* create_graph */ false,
/* depth */ 0,
/* cpu_ready_queue */ global_cpu_ready_queue_,
/* exit_on_error */ true);
// Run BFS to traverse the graph locally. The roots of the graph are
// GraphRoot and all send functions for this autograd context.
std::unordered_set<Node*> seen; // 记录已经访问过的节点
std::queue<Node*> queue; // 一个 Node 类型的 queue
queue.push(static_cast<Node*>(graphRoot.get())); // 插入根对应的Node
auto sendFunctions = autogradContext->sendFunctions(); // 为了获取出边
// 2. 获取出边列表
// Add all the send functions to the queue as roots.
// 普通状态下,root节点内在反向传播时候,已经有了next edges,但是分布式模式下,出边是在sendFunctions之中
for (const auto& mapEntry : sendFunctions) { // sendFunctions就是出边,之前在 addSendFunction之中被添加
// Increment 'outstanding_tasks_' for GraphTask for each send_function
// since we want the local autograd engine to wait for all of them.
graphTask->outstanding_tasks_++; // 出边增加
queue.push(mapEntry.second.get()); // 后续用queue来处理,插入的是 SendRpcBackward
}
// 第二部分,遍历图,计算依赖关系,此时 queue 里面是 root 和 若干 SendRpcBackward
edge_list recvBackwardEdges;
// Traverse the graph.
auto& dependencies = graphTask->dependencies_; // 获取依赖关系
while (!queue.empty()) { // 遍历所有发送边
auto fn = queue.front(); // 得到发送边
queue.pop();
for (const auto& edge : fn->next_edges()) { // 遍历Node(根节点或者SendRpcBackward)的next_edges
if (auto nextFn = edge.function.get()) { // 得到一个边
dependencies[nextFn] += 1; // 对应的节点依赖度加一
const bool wasInserted = seen.insert(nextFn).second; // 是否已经访问过
if (wasInserted) { // 如果true,是插入了,就说明之前没有访问过,否则插不进去,是false
// Seeing this function for the first time.
queue.push(nextFn); // 既然之前没有访问过,就插入到queue
if (nextFn->next_edges().empty()) { // 如果这个边本身没有输出边,说明是叶子节点
TORCH_INTERNAL_ASSERT(
dynamic_cast<AccumulateGrad*>(nextFn) ||
dynamic_cast<RecvRpcBackward*>(nextFn)); // 叶子节点有两种
// We have found a leaf node which should be either AccumulateGrad
// or RecvRpcBackward. Record the function
// to ensure we don't execute it and instead accumulate the grads on
// the autograd context. These functions would be passed in as the
// 'outputs' parameter of the vanilla autograd engine.
// We don't accumulate any grads in the context for RecvRpcBackward.
// RecvRpcBackward is added as an output edge to indicate it is a
// leaf node and this helps in properly computing dependencies for
// the local autograd graph. Putting RecvRpcBackward in
// 'outputEdges' means that this function needs to be executed
// (inline with our assumption for FAST mode that all send/recv
// functions are valid in the backward pass), and as a result all of
// its ancestors need to be executed as well.
if (dynamic_cast<RecvRpcBackward*>(nextFn)) {
recvBackwardEdges.emplace_back(edge); // 特殊处理
}
outputEdges.emplace_back(edge); // 最终输出边
}
}
}
}
}
// 此时,recvBackwardEdges 里面是RecvRpcBackward,outputEdges 里面是 AccumulateGrad
// 以下是第三部分,根据依赖关系找到需要计算那些functions
// Now lets compute which functions need to be executed. The algorithm is as
// follows:
// 1. Create a dummy GraphRoot which points to all 'send' functions for this
// context and the original graphRoot. Run 'init_to_execute' with the
// outputEdges and the dummy GraphRoot. This ensures we mark
// appropriate functions as needed if they are reachable only from a
// specific 'send' function locally and not necessarily from the provided
// roots.
// 2. For all edges in 'outputEdges' which point to 'RecvRpcBackward', mark
// those functions as needed for execution. The reason for this is that
// 'init_to_execute', will mark these as not needed. But 'RecvRpcBackward'
// is unique in the sense that we use it as a leaf node in graph to compute
// needed execution accurately, but unlike AccumulateGrad, we do need to
// execute this function.
if (!outputEdges.empty()) {
// Compute 'needed execution' starting from all 'send' functions and the
// original graphRoot.
edge_list edges;
// Create some dummy edges (input_nr not important for init_to_execute).
for (const auto& mapEntry : sendFunctions) { // 遍历
edges.emplace_back(mapEntry.second, 0); // 得到出边列表
}
// Add the original graphRoot as an edge.
edges.emplace_back(graphRoot, 0); // root也加入出边列表
// Create a dummy GraphRoot and run init_to_execute with it.
GraphRoot dummyRoot(edges, {}); // 建立一个虚拟Root
// 如果出边不为空,则会调用 init_to_execute 对GraphTask进行初始化
graphTask->init_to_execute(dummyRoot, outputEdges, /*accumulate_grad=*/false, /*min_topo_nr=*/0);
// exec_info_ 的数据结构是std::unordered_map<Node*, ExecInfo>
for (auto& mapEntry : graphTask->exec_info_) {
auto& execInfo = mapEntry.second;
if (!execInfo.captures_) { // 看看此张量是否在所求梯度的张量路径上
continue;// 如果不在路径之上,就跳到下一个张量
}
auto fn = mapEntry.first; // 拿到 Node
// There may be nodes other than 'AccumulateGrad', e.g. RecvRPCBackward,
// to be captured.
if (auto accumulateGradFn = dynamic_cast<AccumulateGrad*>(fn)) {
// 如果是叶子节点
for (auto& capture : *execInfo.captures_) { // 遍历张量路径上的节点
capture.hooks_.push_back(
std::make_unique<DistAccumulateGradCaptureHook>( // 给张量插入Hook
std::dynamic_pointer_cast<AccumulateGrad>(
accumulateGradFn->shared_from_this()),
autogradContext));
}
}
}
// Mark all 'RecvRPCBackward' as needing execution.
// RecvRPCBackward需要执行
for (const auto& recvBackwardEdge : recvBackwardEdges) {
graphTask->exec_info_[recvBackwardEdge.function.get()].needed_ = true;
}
}
// Let autograd context take ownership of the GraphTask.
// 设定在上下文之中
autogradContext->setGraphTask(std::move(graphTask));
}
5.2 Подготовка к части 1
5.2.1 Реализация
Поскольку при этом вычисляются локальные зависимости, обход необходимо рассчитывать из корня и локального SendRpcBackward. Сначала проведем подготовительную работу:
- Сначала сгенерируйте GraphTask, но нет необходимости передавать cpu_ready_queue в GraphTask, потому что позже мы будем использовать execute_graph_task_until_ready_queue_empty, где для каждого вызова будет устанавливаться отдельная ReadyQueue.
- Во-вторых, используйте visible для записи посещенных узлов.
- Построение очереди типа Node, корневой узел вставляется в очередь.
- Затем получите исходящие функции из контекста и поместите их в sendFunctions.
- sendFunctions — это исходящий край, который ранее был добавлен в addSendFunction.
- В обычном состоянии у корневого узла уже есть следующие ребра при обратном распространении, но в распределенном режиме исходящие ребра находятся в sendFunctions.
- Перейдите ребра sendFunctions, чтобы создать список ребер для каждого элемента в sendFunctions:
- Количество исходящих ребер GraphTask увеличивается на graphTask->outstanding_tasks_++.
- Вставьте SendRpcBackward в sendFunctions в очереди.
- Наконец, внутри очереди находится root и несколько SendRpcBackwards.
5.2.2 Корреляция
В реализации используются некоторые функции или переменные-члены, и мы выбираем ключевые моменты для введения.
5.2.2.1 sendFunctions
sendFunctions — это функция sendAutogradFunctions_, получившая контекст, который представляет собой std::unordered_map
std::unordered_map<int64_t, std::shared_ptr<SendRpcBackward>>
DistAutogradContext::sendFunctions() const {
std::lock_guard<std::mutex> guard(lock_);
return sendAutogradFunctions_;
}
sendFunctions — это исходящий край, который был добавлен в addSendFunction ранее, и addSendRpcBackward вызовет addSendFunction.
5.2.2.2 outstanding_tasks_
Используйте graphTask->outstanding_tasks_++, чтобы увеличить количество исходящих ребер GraphTask.
GraphTask
выдающиеся_задачи_ — это переменная-член GraphTask.
- выдающиеся_задачи_: используется для записи текущего количества задач, если число равно 0, задача завершена. Если это число не равно 0, GraphTask все равно необходимо запустить.
vania engine
В движке vania есть незавершенные_задачи_.
Это количество NodeTask для обработки, которое используется для определения необходимости выполнения GrapTask.Если число равно 0, задача завершена.
- При создании GraphTask это значение равно 0.
- Outstanding_tasks_ увеличивается на 1, если NodeTask отправляется в ReadyQueue.
- Если рабочий поток выполняет функцию Assessment_function(task) один раз, значение выдающихся_задач уменьшается на 1.
- Если это число не равно 0, GraphTask все равно необходимо запустить.
bool GraphTask::completed() {
return outstanding_tasks_.load() == 0 ||
(exit_on_error_ && has_error_.load());
}
Когда добавляются задачи NodeTask, число незавершенных_задач_ увеличивается на единицу.
dist engine
При расчете зависимостей проходим через sendFunctions, а если в контексте есть несколько SendRpcBackwards, добавляем несколько выдающихся_задач_, каждое дополнительное исходящее ребро означает еще один процесс расчета.
std::unordered_map<int64_t, std::shared_ptr<SendRpcBackward>>
DistAutogradContext::sendFunctions() const {
std::lock_guard<std::mutex> guard(lock_);
return sendAutogradFunctions_;
}
Во время выполнения как void DistEngine::execute_graph_task_until_ready_queue_empty, так и Engine::thread_main уменьшат число незавершенных_задач_.
5.3 Часть II. Вычислительные зависимости
Вторая часть — обход графа и вычисление зависимостей.
5.3.1 Реализация
На данный момент в очереди есть root и несколько SendRpcBackwards, поэтому следующим шагом будет всплывающее окно Node из очереди для расчета. Конкретная логика такова:
- Пройдите все передающие ребра (извлекая узел из очереди), для каждого узла пройдите next_edges узла (корневой узел или SendRpcBackward):
- Если можно получить преимущество, то:
- Соответствующая зависимость узла увеличивается на единицу.
- Если к нему ранее не обращались, оно вставляется в очередь.
- Если само ребро не имеет выходного ребра, это означает, что оно является конечным узлом.Существует два типа конечных узлов: AccumulateGrad или RecvRpcBackward.
- Выполните специальную обработку для recvBackwardEdges.emplace_back(edge).
- Вставьте в конечное выходное ребро outputEdges, обратите внимание, что здесь также вставляется RecvRpcBackward.
- Если можно получить преимущество, то:
После этого локальная переменная recvBackwardEdges содержит RecvRpcBackward, а outputEdges — AccumulateGrad и RecvRpcBackward.
5.3.2 Типы листовых узлов
Существует два вида листовых узлов, поэтому с ними нужно обращаться отдельно.
- AccumulateGrad : Обычный конечный узел является локальным конечным узлом.
- RecvRpcBackward : в прямом графе это принимающий узел RPC.
Из конструкторской документации имеется следующая корреспонденция: "
Мы нашли конечный узел, он должен быть AccumulateGrad или RecvRpcBackward. Мы документируем функцию, чтобы убедиться, что мы не выполняем ее, а вместо этого накапливаем градиенты в контексте автоградации. Эти функции будут переданы в движок ванильного автограда как «выходные» параметры.
Мы не накапливаем никаких градиентов в контексте RecvRpcBackward. RecvRpcBackward добавляется в качестве выходного ребра, чтобы указать, что это конечный узел, что помогает правильно вычислить зависимости локального графа autograd. Помещение RecvRpcBackward в «outputEdges» означает, что эта функция должна быть выполнена (в соответствии с нашим предположением о быстром режиме, что все функции отправки/получения допустимы при обратном распространении), и поэтому все ее функции-предки также должны быть выполнены.
Например, для работы 1 recv — это конечный узел, который представляет собой RecvRpcBackward, которому необходимо передать градиент рабочему процессу 0. Для рабочего 0 подграфы выше, t1, t2, также являются конечными узлами, все из которых являются AccumulateGrad.
5.4 Часть 3 Получить функции
Эта часть определяет, какие функции необходимо вычислить на основе зависимостей.
5.4.1 Алгоритмы
Теперь посчитаем, какие функции нужно выполнить. Алгоритм следующий:
-
- Создайте фиктивный GraphRoot, который указывает на этот контекст и все функции «отправки» исходного GraphRoot. Запустите «init_to_execute» с outputEdges и фиктивным GraphRoot. Это гарантирует, что мы помечаем соответствующие функции по мере необходимости: если они доступны только из локальной конкретной функции «отправить», а не из предоставленного корня.
-
- Для всех ребер в "outputEdges", которые указывают на "RecvRpcBackward", пометьте эти функции как необходимые для выполнения. Причина в том, что "init_to_execute" пометит их как ненужные. Но «RecvRpcBackward» уникален тем, что мы используем его как конечный узел в графе, чтобы точно рассчитать, что необходимо для выполнения, но, в отличие от AccumageGrad, нам нужно выполнить эту функцию.
Конкретно:
- RecvRpcBackward должен быть выполнен.
- AccumulateGrad необходимо накапливать градиенты.
5.4.2 Реализация
На данный момент recvBackwardEdges содержит RecvRpcBackward, а outputEdges содержит AccumulateGrad и RecvRpcBackward. Нам нужно определить, как выполнять последующие выполнения на основе этой информации. Конкретная реализация:
-
Сначала вычислите AccumulateGrad.Если outputEdges не пуст, вставьте информацию outputEdges в GraphTask.exec_info_:
- Построение ребер edge_list — это список исходящих ребер.
- Перейдите через sendFunctions, получите выходной список и добавьте его к краям.
- root также добавляется в список исходящих ребер.
- Создайте виртуальный корень.
- Если исходящий край не пуст, будет вызвана функция init_to_execute для инициализации GraphTask.
- Пройдите exec_info GraphTask, структура данных exec_info_ std::unordered_map
. - Посмотрите, находится ли этот тензор на пути тензора искомого градиента.
- Если не выше пути, перейдите к следующему тензору.
- Получите узел exec_info_.
- Если узел является конечным узлом.
- Пройдите узлы на тензорном пути.
- Вставьте хуки в тензоры. Вот ключ, то есть к тензору, соответствующему AccumulateGrad, добавляется Hook для последующего накопления градиентов.
-
Перебрать recvBackwardEdges для каждого recvBackward,Установите «необходимо выполнить» над соответствующей записью в GraphTask.exec_info_.
На этом этапе зависимости обрабатываются, и вся информация о функциях, которую необходимо вычислить, находится в GraphTask.exec_info_., мы увидим, как это реализовать в следующей статье.
5.5 Резюме
Подытожим логику вычисления зависимостей:
- ComputeDependencies запускает вычисление зависимостей.
- Получите sendAutogradFunctions_ из DistAutogradContext и поместите SendRpcBackward в sendFunctions. В обычном состоянии у корневого узла уже есть следующие ребра при обратном распространении, но в распределенном режиме исходящие ребра находятся в sendFunctions, поэтому их необходимо извлечь и поместить в следующую очередь.
- Пройдите через sendFunctions и добавьте Node в очередь.На данный момент очередь является корневой и некоторые SendRpcBackward.
- Пройдите очередь для обработки, и результатом обработки будут две локальные переменные edge_list. RecvBackwardEdges содержит RecvRpcBackward, а outputEdges содержит AccumulateGrad и RecvRpcBackward. Нам нужно определить, как его выполнить в соответствии с этой информацией.
- Просмотрите recvBackwardEdges и outputEdges и добавьте соответствующую информацию в
GraphTask.exec_info_
, пока что зависимости обрабатываются, и вся информация о функциях, которую необходимо вычислить, находится в GraphTask.exec_info_.- В Hook добавлен AccumulateGrad для последующего накопления градиентов.
- RecvRpcBackward настроен на выполнение.
computeDependencies
+
+---------------------------+ | 1
| DistAutogradContext | |
| | v
| | 2
| sendAutogradFunctions_ +-------> map<int,SendRpcBackward> > sendFunctions
| |
+---------------------------+ +
|
| 3
v
queue<Node*> queue
+
| 4
|
|
v
recvBackwardEdges = [RecvRpcBackward 1, RecvRpcBackward 2, ...]
outputEdges = [RecvRpcBackward 1, RecvRpcBackward 2,
AccumulateGrad 1, AccumulateGrad 2, ...]
+
|
| 5
v
GraphTask.exec_info_
0xEE Личная информация
★★★★★★Думая о жизни и технологиях★★★★★★
Публичный аккаунт WeChat:мысли Росси
ссылка 0xFF
Понимать распределенное обучение интерпретации исходного кода PyTorch?
py torch.Apache can.org/docs/1.7/59…
py torch.org/docs/master… py torch.org/docs/master…
Ууху. Моя школа 3С. Талант /пи факел/Пак Ючон…
PyTorch Распределенный Autograd Design
Getting started with Distributed RPC Framework
Implementing a Parameter Server using Distributed RPC Framework
Combining Distributed DataParallel with Distributed RPC Framework