[源码解析] PyTorch 分布式 Autograd (2) ---- RPC基础

0x00 摘要

前文我们给出了分布式autograd的设计思路,本文开始,我们进行具体源码分析。因为无论是前向传播还是反向传播,都需要依赖 RPC 来完成,所以我们先看看封装于 RPC 之上的一些基本功能,比如初始化,代理(RPC 相关功能都是基于代理完成),消息接受,发送等等。

通过本文,大家可以了解:如何初始化RPC后端,如何生成 RPC 代理,如何使用RPC代理进行发送和接受消息,如何连接远端 dist.autograd 自动微分引擎。

PyTorch分布式其他文章如下:

深度学习利器之自动微分(1)

深度学习利器之自动微分(2)

[源码解析]深度学习利器之自动微分(3) --- 示例解读

[源码解析]PyTorch如何实现前向传播(1) --- 基础类(上)

[源码解析]PyTorch如何实现前向传播(2) --- 基础类(下)

[源码解析] PyTorch如何实现前向传播(3) --- 具体实现

[源码解析] Pytorch 如何实现后向传播 (1)---- 调用引擎

[源码解析] Pytorch 如何实现后向传播 (2)---- 引擎静态结构

[源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑

[源码解析] PyTorch 如何实现后向传播 (4)---- 具体算法

[源码解析] PyTorch 分布式(1)------历史和概述

[源码解析] PyTorch 分布式(2) ----- DataParallel(上)

[源码解析] PyTorch 分布式(3) ----- DataParallel(下)

[源码解析] PyTorch 分布式(4)------分布式应用基础概念

[源码解析] PyTorch分布式(5) ------ DistributedDataParallel 总述&如何使用

[源码解析] PyTorch分布式(6) ---DistributedDataParallel -- 初始化&store

[源码解析] PyTorch 分布式(7) ----- DistributedDataParallel 之进程组

[源码解析] PyTorch 分布式(8) -------- DistributedDataParallel之论文篇

[源码解析] PyTorch 分布式(9) ----- DistributedDataParallel 之初始化

[源码解析] PyTorch 分布式(10)------DistributedDataParallel 之 Reducer静态架构

[源码解析] PyTorch 分布式(11) ----- DistributedDataParallel 之 构建Reducer和Join操作

[源码解析] PyTorch 分布式(12) ----- DistributedDataParallel 之 前向传播

[源码解析] PyTorch 分布式(13) ----- DistributedDataParallel 之 反向传播

[源码解析] PyTorch 分布式 Autograd (1) ---- 设计

为了更好的说明,本文代码会依据具体情况来进行相应精简。

0x01 示例

我们从 PyTorch 示例部分之中摘录示例代码并且修改了一些,代码目的是让两个 worker 之间就通过 RPC 进行协作。示例 worker 具体分为两部分:

  • RPC操作,构建依赖基础。
  • 执行后向传播。
  1. def my_add(t1, t2):
  2. return torch.add(t1, t2)
  3. def worker0():
  4. # On worker 0:
  5. # Setup the autograd context. Computations that take
  6. # part in the distributed backward pass must be within
  7. # the distributed autograd context manager.
  8. with dist_autograd.context() as context_id:
  9. t1 = torch.rand((3, 3), requires_grad=True)
  10. t2 = torch.rand((3, 3), requires_grad=True)
  11. # 第一阶段:RPC操作,构建依赖基础
  12. # Perform some computation remotely.
  13. t3 = rpc.rpc_sync("worker1", my_add, args=(t1, t2))
  14. # Perform some computation locally based on remote result.
  15. t4 = torch.rand((3, 3), requires_grad=True)
  16. t5 = torch.mul(t3, t4)
  17. # Compute some loss.
  18. loss = t5.sum()
  19. # 第二阶段,执行后向传播
  20. # Run the backward pass.
  21. dist_autograd.backward(context_id, [loss])
  22. # Retrieve the gradients from the context.
  23. dist_autograd.get_gradients(context_id)
  24. print(loss)

可以用如下办法来启动了两个 worker,其中使用了 rpc.init_rpc 来初始化 rpc。worker0 会启动,然后利用 RPC 在 worker 1 之上也进行了一些操作。

  1. def run_worker(rank, world_size):
  2. r"""
  3. A wrapper function that initializes RPC, calls the function, and shuts down
  4. RPC.
  5. """
  6. # We need to use different port numbers in TCP init_method for init_rpc and
  7. # init_process_group to avoid port conflicts.
  8. rpc_backend_options = TensorPipeRpcBackendOptions()
  9. rpc_backend_options.init_method = "tcp://localhost:29501"
  10. # Rank 0 and 1 are trainers.
  11. if rank == 0:
  12. rpc.init_rpc(
  13. "worker0",
  14. rank=rank,
  15. world_size=world_size,
  16. rpc_backend_options=rpc_backend_options,
  17. )
  18. worker0()
  19. elif rank == 1:
  20. rpc.init_rpc(
  21. "worker1",
  22. rank=rank,
  23. world_size=world_size,
  24. rpc_backend_options=rpc_backend_options,
  25. )
  26. # block until all rpcs finish
  27. rpc.shutdown()

0x02 RPC 基础

2.1 初始化

我们从头看看示例代码,当脚本启动时候,会调用到 rpc.init_rpc 来初始化 rpc。从 RPC 注释中可以看到两个概念,就是大家常见的 rank 和 world_size。

  1. rank (int): a globally unique id/rank of this node.
  2. world_size (int): The number of workers in the group.

具体初始化代码是:

  1. def init_rpc(
  2. name,
  3. backend=None,
  4. rank=-1,
  5. world_size=None,
  6. rpc_backend_options=None,
  7. ):
  8. dist_autograd._init(rank) # 我们后续会讨论分布式自动微分引擎
  9. _set_profiler_node_id(rank)
  10. # Initialize RPC.
  11. _init_rpc_backend(backend, store, name, rank, world_size, rpc_backend_options)

其中我们关心的是:_init_rpc_backend 会设定后端。

2.1.1 初始化后端

_init_rpc_backend 这里会依据配置来看看最后生成什么 Agent,然后把这个代理设定到当前上下文。RPC有两种后端,TENSORPIPE 和 PROCESS_GROUP,其中PROCESS_GROUP已经被废弃,会逐渐迁移到TENSORPIPE。

  1. def _init_rpc_backend(
  2. backend=BackendType.TENSORPIPE, # 默认后端是TENSORPIPE
  3. store=None,
  4. name=None,
  5. rank=-1,
  6. world_size=-1,
  7. rpc_backend_options=None,
  8. ):
  9. _validate_rpc_args(backend, store, name, rank, world_size, rpc_backend_options)
  10. if _is_current_rpc_agent_set():
  11. raise RuntimeError("RPC is already initialized")
  12. # Initialize RPC.
  13. rpc_agent = backend_registry.init_backend( # 生成一个agent
  14. backend,
  15. store=store,
  16. name=name,
  17. rank=rank,
  18. world_size=world_size,
  19. rpc_backend_options=rpc_backend_options,
  20. )
  21. api._init_rpc_states(rpc_agent) # 设定代理到当前上下文

可以看到,默认会生成 TensorPipeAgent。

2.1.2 生成代理

我们接下来看看如何生成 TensorPipeAgent,具体是在 torch/csrc/distributed/rpc/init.cpp。当这里生成 TensorPipeAgent 时候,把 RequestCallbackImpl 配置为回调函数。代理内部就用这个回调函数用来处理接收到的请求

  1. shared_ptr_class_<TensorPipeAgent>(module, "TensorPipeAgent", rpcAgent)
  2. .def(
  3. py::init([](const c10::intrusive_ptr<::c10d::Store>& store,
  4. std::string selfName,
  5. worker_id_t selfId,
  6. int worldSize,
  7. c10::intrusive_ptr<::c10d::ProcessGroup> processGroup,
  8. TensorPipeRpcBackendOptions opts) {
  9. return std::shared_ptr<TensorPipeAgent>(
  10. new TensorPipeAgent(
  11. store,
  12. std::move(selfName),
  13. selfId,
  14. worldSize,
  15. std::move(processGroup),
  16. std::move(opts),
  17. std::make_unique<RequestCallbackImpl>()), // RequestCallbackImpl 被配置到 Agent 之上
  18. impl::destroy_without_gil<TensorPipeAgent>);
  19. })

具体如下:

  1. +-----------------+ +-----------------------+
  2. | TensorPipeAgent | | RequestCallbackImpl |
  3. | | | |
  4. | cb_ +----------> | |
  5. | | | |
  6. +-----------------+ +-----------------------+

2.1.3 设置代理

_init_rpc_states 会把代理设置在PyTorch环境之中,其定义在 torch/distributed/rpc/api.py 之中有。

  1. def _init_rpc_states(agent):
  2. worker_infos = agent.get_worker_infos()
  3. global _ALL_WORKER_NAMES
  4. _ALL_WORKER_NAMES = {worker_info.name for worker_info in worker_infos}
  5. # NB: backend implementation might have already set the rpc_agent.
  6. if not _is_current_rpc_agent_set():
  7. _set_and_start_rpc_agent(agent)

接下来就要进入了C++世界。在 torch/csrc/distributed/rpc/init.cpp 中有 _set_and_start_rpc_agent,其作用是:

  • RpcAgent::setCurrentRpcAgent 设定了代理。
  • 调用 rpcAgent->start() 来启动代理。
  1. module.def(
  2. "_set_and_start_rpc_agent",
  3. [](const std::shared_ptr<RpcAgent>& rpcAgent) {
  4. RpcAgent::setCurrentRpcAgent(rpcAgent); // 这里设定了 Agent
  5. // Initializing typeResolver inside RpcAgent constructor will make
  6. // RpcAgent have python dependency. To avoid RpcAgent to have python
  7. // dependency, setTypeResolver() here.
  8. std::shared_ptr<TypeResolver> typeResolver =
  9. std::make_shared<TypeResolver>([&](const c10::QualifiedName& qn) {
  10. auto typePtr = PythonRpcHandler::getInstance().parseTypeFromStr(
  11. qn.qualifiedName());
  12. return c10::StrongTypePtr(
  13. PythonRpcHandler::getInstance().jitCompilationUnit(),
  14. std::move(typePtr));
  15. });
  16. rpcAgent->setTypeResolver(typeResolver);
  17. rpcAgent->start(); // 启动代理
  18. },
  19. py::call_guard<py::gil_scoped_release>());

setCurrentRpcAgent 定义在 torch/csrc/distributed/rpc/rpc_agent.cpp 之中。

2.1.4 静态类变量

在 RpcAgent 之中,有一个静态成员变量 currentRpcAgent_。

  1. class TORCH_API RpcAgent {
  2. // 我们省略了其他成员变量和函数
  3. private:
  4. static std::shared_ptr<RpcAgent> currentRpcAgent_;
  5. }

在 C++ 之中,静态成员变量有如下特点:

  • 其属于整个类所有。
  • 其生命期不依赖于任何对象,为程序的生命周期。
  • 可以通过类名直接访问公有静态成员变量。
  • 可以通过对象名访问一个类的公有静态成员变量。
  • 类的所有派生对象共享该类的静态成员变量。
  • 静态成员变量需要在该类外单独分配空间。
  • 静态成员变量在程序内部位于全局数据区。

所以,我们可知RpcAgent::currentRpcAgent_ 可以认为就是全局变量,rpc 统一使用这个变量进行协调。具体通过 RpcAgent 的一些公有成员函数来完成这些功能。

  1. std::shared_ptr<RpcAgent> RpcAgent::currentRpcAgent_ = nullptr;
  2. bool RpcAgent::isCurrentRpcAgentSet() {
  3. return std::atomic_load(&currentRpcAgent_) != nullptr;
  4. }
  5. std::shared_ptr<RpcAgent> RpcAgent::getCurrentRpcAgent() {
  6. std::shared_ptr<RpcAgent> agent = std::atomic_load(&currentRpcAgent_);
  7. return agent;
  8. }
  9. void RpcAgent::setCurrentRpcAgent(std::shared_ptr<RpcAgent> rpcAgent) {
  10. if (rpcAgent) {
  11. std::shared_ptr<RpcAgent> previousAgent;
  12. // Use compare_exchange so that we don't actually perform the exchange if
  13. // that would trigger the assert just below. See:
  14. // https://en.cppreference.com/w/cpp/atomic/atomic_compare_exchange
  15. std::atomic_compare_exchange_strong(
  16. &currentRpcAgent_, &previousAgent, std::move(rpcAgent));
  17. } else {
  18. // We can't use compare_exchange (we don't know what value to expect) but we
  19. // don't need to, as the only case that would trigger the assert is if we
  20. // replaced nullptr with nullptr, which we can just do as it has no effect.
  21. std::shared_ptr<RpcAgent> previousAgent =
  22. std::atomic_exchange(&currentRpcAgent_, std::move(rpcAgent));
  23. }
  24. }

于是目前拓展如下,以后进行 RPC 操作,都会通过 RpcAgent::currentRpcAgent_ 这个全局变量进行。

  1. RpcAgent::currentRpcAgent_
  2. +
  3. |
  4. |
  5. |
  6. v
  7. +-----+-----------+ +-----------------------+
  8. | TensorPipeAgent | | RequestCallbackImpl |
  9. | | | |
  10. | cb_ +----------> | |
  11. | | | |
  12. +-----------------+ +-----------------------+

2.2 RPC 代理

dist.autograd 的相关功能都是基于 RPC 代理完成,所以我们需要仔细看看代理。

2.2.1 RpcAgent

这是用来传递RPC的代理,是收发 RPC消息的代理基类,其:

  • 提供了send API用来处理request 和 response。
  • 也配置了 cb_ 用来处理接收到的请求。

WorkerInfo 是代理实例所在 worker 的全局唯一标示,包括name_id_这两个成员变量。name_是全局唯一名字,id_是全局唯一ID。

  1. class TORCH_API RpcAgent {
  2. public:
  3. RpcAgent(
  4. WorkerInfo id,
  5. std::unique_ptr<RequestCallback> cb,
  6. std::chrono::milliseconds rpcTimeout);
  7. // 给 to.id 代表的其他 RpcAgengt 发送一个消息,返回一个JitFuture,这个实现是异步的。
  8. virtual c10::intrusive_ptr<JitFuture> send(
  9. const WorkerInfo& to.id,
  10. Message&& message,
  11. const float rpcTimeoutSeconds = kUnsetRpcTimeout,
  12. const std::unordered_map<c10::Device, c10::Device>& deviceMap = {}) = 0;
  13. protected:
  14. const WorkerInfo workerInfo_; // 代理实例的全局唯一标示
  15. const std::unique_ptr<RequestCallback> cb_; // 回调函数
  16. std::atomic<std::chrono::milliseconds> rpcTimeout_;
  17. std::atomic<bool> profilingEnabled_;
  18. std::shared_ptr<TypeResolver> typeResolver_;
  19. std::atomic<bool> rpcAgentRunning_;
  20. private:
  21. static std::shared_ptr<RpcAgent> currentRpcAgent_; // 全局代理
  22. // Add GIL wait time data point to metrics
  23. virtual void addGilWaitTime(const std::chrono::microseconds gilWaitTime) = 0;
  24. friend class PythonRpcHandler;
  25. // Condition Variable to signal when the rpcRetryMap_ has been populated.
  26. std::condition_variable rpcRetryMapCV_;
  27. // Mutex to protect RpcRetryMap_.
  28. std::mutex rpcRetryMutex_;
  29. };

2.2.2 ProcessGroupAgent

ProcessGroupAgent 是 RpcAgent 的派生类。这是之前使用的,但是 PyTorch 提供了更优秀的 TensorAgent。我们只选取了部分成员变量。

  1. class TORCH_API ProcessGroupAgent : public RpcAgent {
  2. public:
  3. c10::intrusive_ptr<::c10d::ProcessGroup> pg_;
  4. // worker name -> rank
  5. std::unordered_map<std::string, worker_id_t> nameMap_;
  6. std::vector<WorkerInfo> allWorkerInfo_;
  7. MessageCounter sendCounts_;
  8. MessageCounter recvCounts_;
  9. std::atomic<int64_t> nextId_;
  10. std::thread listenerThread_;
  11. std::thread futureTimeoutThread_;
  12. c10::intrusive_ptr<c10d::ProcessGroup::Work> recvWork_;
  13. std::unordered_map<
  14. worker_id_t,
  15. std::set<c10::intrusive_ptr<c10d::ProcessGroup::Work>>>
  16. currentPendingSends_;
  17. ThreadPool threadPool_;
  18. // Mapping of request id to FutureInfo struct.
  19. std::unordered_map<int64_t, FutureInfo> futures_;
  20. };

2.2.3 TensorPipeAgent

TensorPipeAgent 定义在 torch/csrc/distributed/rpc/tensorpipe_agent.h,这是目前和未来使用的。TensorPipeAgent利用TensorPipe在可用传输或通道之中透明地移动张量和数据。它就像一个混合的RPC传输,提供共享内存(linux)和TCP(linux&mac)支持。PyTorch 正在开发其支持CUDA版本。

我们只选取了部分成员变量。

  1. // TensorPipeAgent leverages TensorPipe (https://github.com/pytorch/tensorpipe)
  2. // to transparently move tensors and payloads through the fastest available
  3. // transport or channel. It acts like a hybrid RPC transport, providing shared
  4. // memory (linux) and TCP (linux & mac) support. CUDA support is in progress.
  5. class TensorPipeAgent : public RpcAgent {
  6. public:
  7. TensorPipeAgent(
  8. const c10::intrusive_ptr<::c10d::Store>& store,
  9. std::string selfName,
  10. worker_id_t selfId,
  11. int worldSize,
  12. c10::intrusive_ptr<::c10d::ProcessGroup> processGroup,
  13. TensorPipeRpcBackendOptions opts,
  14. std::unique_ptr<RequestCallback> cb);
  15. const TensorPipeRpcBackendOptions opts_;
  16. std::unordered_map<std::string, DeviceMap> reverseDeviceMaps_;
  17. std::vector<c10::Device> devices_;
  18. ThreadPool threadPool_;
  19. std::shared_ptr<tensorpipe::Context> context_;
  20. std::shared_ptr<tensorpipe::Listener> listener_;
  21. mutable std::mutex connectedPipesMutex_;
  22. std::unordered_map<worker_id_t, ClientPipe> connectedPipes_;
  23. // Maps keyed on name and id for easy WorkerInfo lookup.
  24. std::unordered_map<worker_id_t, WorkerInfo> workerIdToInfo_;
  25. std::unordered_map<std::string, WorkerInfo> workerNameToInfo_;
  26. std::unordered_map<std::string, std::string> workerNameToURL_;
  27. ::c10d::PrefixStore rankToNameStore_;
  28. ::c10d::PrefixStore nameToAddressStore_;
  29. const int worldSize_;
  30. // The join method is required to behave like a barrier and perform collective
  31. // operations. For simplicity and reliability, we offload this to a process
  32. // group, but probably one day we might want to re-implement them using RPCs.
  33. const c10::intrusive_ptr<::c10d::ProcessGroup> processGroup_;
  34. std::atomic<uint64_t> nextMessageID_{0};
  35. // Thread that will poll the timeoutMap_ for timed out messages and mark them
  36. // with an error accordingly
  37. std::thread timeoutThread_;
  38. // Function run by the timeoutThread_ to check for timed out RPCs
  39. void pollTimeoutRpcs();
  40. };

2.2.4 回调函数

Agent 在收到消息时候,会调用回调函数。而 RequestCallbackImpl 实现了回调逻辑。RequestCallbackImpl 是派生类,我们先来看看基类 RequestCallbackNoPython,结果找到了RequestCallback 这个接口,所以 RequestCallback 才是这个派生体系的基础。

  1. class TORCH_API RequestCallbackNoPython : public RequestCallback
  2. class TORCH_API RequestCallbackImpl : public RequestCallbackNoPython
2.2.4.1 RequestCallback

RequestCallback 是处理 RPC 消息的接口,是一个抽象类。

  1. // Functor which is invoked to process an RPC message. This is an abstract class
  2. // with some common functionality across all request handlers. Users need to
  3. // implement this interface to perform the actual business logic.
  4. class TORCH_API RequestCallback {
  5. public:
  6. // Invoke the callback.
  7. c10::intrusive_ptr<JitFuture> operator()(
  8. Message& request,
  9. std::shared_ptr<LazyStreamContext> ctx) const;
  10. // NOLINTNEXTLINE(modernize-use-equals-default)
  11. virtual ~RequestCallback() {}
  12. protected:
  13. // RpcAgent implementation should invoke ``RequestCallback`` to process
  14. // received requests. There is no restriction on the implementation's
  15. // threading model. This function takes an rvalue reference of the Message
  16. // object. It is expected to return the future to a response message or
  17. // message containing an exception. Different rpc agent implementations are
  18. // expected to ensure delivery of the response/exception based on their
  19. // implementation specific mechanisms.
  20. virtual c10::intrusive_ptr<JitFuture> processMessage(
  21. Message& request,
  22. std::shared_ptr<LazyStreamContext> ctx) const = 0;
  23. };
2.2.4.2 RequestCallbackNoPython

RequestCallbackNoPython 的定义在 torch/csrc/distributed/rpc/request_callback_no_python.h,其实现了一些处理机制,因为其包含太多方法,我们只能摘录部分,如果有兴趣的朋友请深入研究。

  1. // RequestCallback implementation with no Python dependencies.
  2. class TORCH_API RequestCallbackNoPython : public RequestCallback {
  3. public:
  4. c10::intrusive_ptr<JitFuture> processMessage(
  5. Message& request,
  6. std::shared_ptr<LazyStreamContext> ctx) const override;
  7. protected:
  8. void processForwardAutogradReq(
  9. RpcCommandBase& rpc,
  10. const int64_t messageId,
  11. const c10::intrusive_ptr<JitFuture>& responseFuture,
  12. std::shared_ptr<LazyStreamContext> ctx) const;
  13. void processBackwardAutogradReq(
  14. RpcCommandBase& rpc,
  15. const int64_t messageId,
  16. const c10::intrusive_ptr<JitFuture>& responseFuture) const;
  17. void processRpc(
  18. RpcCommandBase& rpc,
  19. const MessageType& messageType,
  20. const int64_t messageId,
  21. const c10::intrusive_ptr<JitFuture>& responseFuture,
  22. std::shared_ptr<LazyStreamContext> ctx) const;
  23. virtual void processRpcWithErrors(
  24. RpcCommandBase& rpc,
  25. const MessageType& messageType,
  26. const int64_t messageId,
  27. const c10::intrusive_ptr<JitFuture>& responseFuture,
  28. std::shared_ptr<LazyStreamContext> ctx) const;
  29. virtual void processRRefBackward(
  30. RpcCommandBase& rpc,
  31. const int64_t messageId,
  32. const c10::intrusive_ptr<JitFuture>& responseFuture) const;
  33. };

我们会在后续分析接受逻辑时候,看到如何调用到回调函数。

0x03 发送逻辑

我们先来看看发送逻辑。也就是 rpc.rpc_sync 的作用:建立 root,添加 send等。

3.1 Python

我们从 python 部分开始。

  1. # Perform some computation remotely.
  2. t3 = rpc.rpc_sync("worker1", my_add, args=(t1, t2))

首先来到 rpc_sync,发现其调用了_invoke_rpc。

  1. @_require_initialized
  2. def rpc_sync(to, func, args=None, kwargs=None, timeout=UNSET_RPC_TIMEOUT):
  3. fut = _invoke_rpc(to, func, RPCExecMode.SYNC, args, kwargs, timeout)
  4. return fut.wait()

其次来到_invoke_rpc,可以看到此函数依据调用类型不同(内置操作,script,udf这三种),选择了不同路径。

  1. def _invoke_rpc(to, func, rpc_type, args=None, kwargs=None, rpc_timeout=UNSET_RPC_TIMEOUT):
  2. qualified_name = torch.jit._builtins._find_builtin(func)
  3. dst_worker_info = _to_worker_info(to)
  4. should_profile = torch.autograd._profiler_enabled()
  5. ctx_manager = _enable_rpc_profiler(should_profile, qualified_name, func, rpc_type, dst_worker_info)
  6. with ctx_manager as rf:
  7. args = args if args else ()
  8. kwargs = kwargs if kwargs else {}
  9. is_async_exec = hasattr(func, "_wrapped_async_rpc_function")
  10. if is_async_exec:
  11. wrapped = func._wrapped_async_rpc_function
  12. if isinstance(wrapped, torch.jit.ScriptFunction):
  13. func = wrapped
  14. if qualified_name is not None:
  15. fut = _invoke_rpc_builtin( # 内置rpc
  16. dst_worker_info,
  17. qualified_name,
  18. rpc_timeout,
  19. *args,
  20. **kwargs
  21. )
  22. elif isinstance(func, torch.jit.ScriptFunction): # 脚本
  23. fut = _invoke_rpc_torchscript(
  24. dst_worker_info.name,
  25. torch._jit_internal._qualified_name(func),
  26. args,
  27. kwargs,
  28. rpc_timeout,
  29. is_async_exec
  30. )
  31. else:
  32. (pickled_python_udf, tensors) = _default_pickler.serialize(
  33. PythonUDF(func, args, kwargs)
  34. )
  35. fut = _invoke_rpc_python_udf( # 用户udf
  36. dst_worker_info,
  37. pickled_python_udf,
  38. tensors,
  39. rpc_timeout,
  40. is_async_exec
  41. )
  42. if should_profile:
  43. fut = rf._call_end_callbacks_on_future(fut)
  44. return fut

从这里开始就进入到了C++世界,torch/csrc/distributed/rpc/init.cpp。

3.2 C++

这里可以看到 _invoke_rpc_builtin 对应了 pyRpcBuiltin,_invoke_rpc_python_udf 对应了 pyRpcPythonUdf。

  1. PyObject* rpc_init(PyObject* _unused, PyObject* noargs) {
  2. module.def(
  3. "_invoke_rpc_builtin",
  4. [](const WorkerInfo& dst,
  5. const std::string& opName,
  6. const float rpcTimeoutSeconds,
  7. const py::args& args,
  8. const py::kwargs& kwargs) {
  9. return std::make_shared<jit::PythonFutureWrapper>(
  10. pyRpcBuiltin(dst, opName, args, kwargs, rpcTimeoutSeconds)); # 内置函数
  11. },
  12. py::call_guard<py::gil_scoped_acquire>());
  13. module.def(
  14. "_invoke_rpc_python_udf",
  15. [](const WorkerInfo& dst,
  16. std::string& pickledPythonUDF,
  17. std::vector<torch::Tensor>& tensors,
  18. const float rpcTimeoutSeconds,
  19. const bool isAsyncExecution) {
  20. return std::make_shared<jit::PythonFutureWrapper>(pyRpcPythonUdf(
  21. dst,
  22. pickledPythonUDF, # 对应了udf
  23. tensors,
  24. rpcTimeoutSeconds,
  25. isAsyncExecution));
  26. },
  27. py::call_guard<py::gil_scoped_release>());
  28. # 省略其他
  29. }

我们选用 _invoke_rpc_builtin 对应的 pyRpcBuiltin 来看看。

3.2.1 pyRpcBuiltin

在 torch/csrc/distributed/rpc/python_functions.cpp可以看到,pyRpcBuiltin 会调用到 sendMessageWithAutograd。

  1. c10::intrusive_ptr<JitFuture> pyRpcBuiltin(
  2. const WorkerInfo& dst,
  3. const std::string& opName,
  4. const py::args& args,
  5. const py::kwargs& kwargs,
  6. const float rpcTimeoutSeconds) {
  7. DCHECK(PyGILState_Check());
  8. Stack stack;
  9. auto op = matchBuiltinOp(opName, args, kwargs, stack);
  10. // Release GIL since args and kwargs processing is done.
  11. py::gil_scoped_release release;
  12. auto scriptCall = std::make_unique<ScriptCall>(op, std::move(stack));
  13. auto agent = RpcAgent::getCurrentRpcAgent(); // 获取当前agent
  14. return toPyJitFuture(sendMessageWithAutograd( // 发送请求
  15. *agent,
  16. dst,
  17. std::move(*scriptCall).toMessage(),
  18. false,
  19. rpcTimeoutSeconds));
  20. }

3.2.2 sendMessageWithAutograd

在 torch/csrc/distributed/autograd/utils.cpp 这里利用 agent 来进行发送 FORWARD_AUTOGRAD_REQ。

后面在接收方,我们将会看到处理 FORWARD_AUTOGRAD_REQ 消息,因此发送和接受大致可以联系起来。

  1. c10::intrusive_ptr<JitFuture> sendMessageWithAutograd(
  2. RpcAgent& agent,
  3. const WorkerInfo& dst,
  4. torch::distributed::rpc::Message&& wrappedRpcMsg,
  5. bool forceGradRecording,
  6. const float rpcTimeoutSeconds,
  7. bool forceDisableProfiling) {
  8. auto msg = getMessageWithAutograd( // 这里会与上下文交互,构建了 FORWARD_AUTOGRAD_REQ
  9. dst.id_,
  10. std::move(wrappedRpcMsg),
  11. MessageType::FORWARD_AUTOGRAD_REQ,
  12. forceGradRecording,
  13. agent.getDeviceMap(dst));
  14. c10::intrusive_ptr<JitFuture> fut;
  15. // If profiler is enabled, wrap this message with profiling metadata that will
  16. // tell the remote end to process this request with the profiler enabled.
  17. if (!forceDisableProfiling && torch::autograd::profiler::profilerEnabled()) {
  18. auto profilerConfig = torch::autograd::profiler::getProfilerConfig();
  19. auto msgWithProfiling = getMessageWithProfiling(
  20. std::move(msg),
  21. rpc::MessageType::RUN_WITH_PROFILING_REQ, //构建消息
  22. std::move(profilerConfig));
  23. // 发送消息
  24. fut = agent.send(dst, std::move(msgWithProfiling), rpcTimeoutSeconds);
  25. } else {
  26. fut = agent.send(dst, std::move(msg), rpcTimeoutSeconds);
  27. }
  28. return fut;
  29. }

发送流程如下,其中 sendMessageWithAutograd 会使用 RpcAgent::getCurrentRpcAgent() 得到 RpcAgent::currentRpcAgent_,就是得到了全局设置的代理,然后通过代理进行发送。

  1. rpc.rpc_sync
  2. +
  3. |
  4. |
  5. v
  6. _invoke_rpc_builtin
  7. +
  8. | Python
  9. +---------------------------------------------------------------+
  10. | C++
  11. |
  12. v
  13. pyRpcBuiltin
  14. +
  15. |
  16. |
  17. v
  18. sendMessageWithAutograd(RpcAgent::getCurrentRpcAgent())
  19. +
  20. |
  21. |
  22. | RpcAgent::currentRpcAgent_
  23. | +
  24. | |
  25. | |
  26. | v
  27. | +-----+-----------+
  28. | | TensorPipeAgent | +-----------------------+
  29. | | | | RequestCallbackImpl |
  30. | | cb_ +------------> | |
  31. | | | +-----------------------+
  32. | | |
  33. | | |
  34. +-----------> send +-----------> Will send message to other worker
  35. | |
  36. | |
  37. +-----------------+

0x04 接受逻辑

4.1 回调

当Agent接受到消息之后,会调用到RequestCallback::operator()。就是我们前面所说的回调函数。代码位于 torch/csrc/distributed/rpc/tensorpipe_agent.cpp。

  1. void TensorPipeAgent::respond(std::shared_ptr<tensorpipe::Pipe>& pipe) {
  2. pipeRead(
  3. pipe,
  4. [this, pipe](
  5. const tensorpipe::Error& error,
  6. Message&& requestMessage,
  7. std::shared_ptr<LazyStreamContext> ctx) mutable {
  8. // Arm for next read
  9. respond(pipe);
  10. uint64_t messageId = requestMessage.id();
  11. increaseCallCount(serverActiveCalls_);
  12. // Defer user RPC UDF run to thread pool
  13. threadPool_.run([this,
  14. pipe,
  15. messageId,
  16. requestMessage{std::move(requestMessage)},
  17. ctx{std::move(ctx)}]() mutable {
  18. c10::intrusive_ptr<JitFuture> futureResponseMessage;
  19. try {
  20. // 这里会调用 RequestCallback 来进行回调逻辑处理
  21. futureResponseMessage = cb_->operator()(requestMessage, ctx);
  22. } catch (const std::exception& /* unused */) {
  23. futureResponseMessage =
  24. c10::make_intrusive<JitFuture>(at::AnyClassType::get());
  25. futureResponseMessage->setError(std::current_exception());
  26. }
  27. // Shortcut if immediately done
  28. if (futureResponseMessage->completed()) {
  29. decreaseCallCount(serverActiveCalls_);
  30. sendCompletedResponseMessage(
  31. pipe, *futureResponseMessage, messageId, std::move(ctx));
  32. } else {
  33. // Not complete yet
  34. increaseCallCount(serverActiveAsyncCalls_);
  35. futureResponseMessage->addCallback(
  36. [this, pipe, messageId, ctx{std::move(ctx)}](
  37. JitFuture& futureResponseMessage) mutable {
  38. decreaseCallCount(serverActiveCalls_);
  39. decreaseCallCount(serverActiveAsyncCalls_);
  40. sendCompletedResponseMessage(
  41. pipe, futureResponseMessage, messageId, std::move(ctx));
  42. });
  43. }
  44. });
  45. });
  46. }

4.2 operator()

operator() 之中会调用 processMessage 处理消息。

  1. c10::intrusive_ptr<JitFuture> RequestCallback::operator()(
  2. Message& request,
  3. std::shared_ptr<LazyStreamContext> ctx) const {
  4. // NB: cannot clear autograd context id here because the processMessage method
  5. // might pause waiting for all RRefs in the arguments to be confirmed by their
  6. // owners and resumne processing in a different thread. Hence, the
  7. // thread_local context id needs to be set and cleared in the thread that
  8. // indeed carries out the processing logic.
  9. return processMessage(request, std::move(ctx));
  10. }

随后,会调用到 RequestCallbackNoPython::processMessage 之中。

  • 先调用 RequestCallbackImpl 中实现的 deserializePythonRpcCommand 来对 PythonUDF 反序列化。
  • 然后调用 processRpcWithErrors 来处理消息。
  1. c10::intrusive_ptr<JitFuture> RequestCallbackNoPython::processMessage(
  2. Message& request,
  3. std::shared_ptr<LazyStreamContext> ctx) const {
  4. // We need two futures here because it could pause twice when processing a
  5. // RPC message:
  6. // 1) waiting for all RRefs in the arguments to become confirmed;
  7. // 2) waiting for processRpc to finish.
  8. auto retFuture = c10::make_intrusive<JitFuture>(at::AnyClassType::get());
  9. auto& rrefContext = RRefContext::getInstance();
  10. try {
  11. rrefContext.recordThreadLocalPendingRRefs();
  12. // Deserialize PythonUDF here to trigger RRef unpickling
  13. // 调用 RequestCallbackImpl 中实现的 deserializePythonRpcCommand 来对 PythonUDF 反序列化
  14. std::unique_ptr<RpcCommandBase> rpc = deserializePythonRpcCommand(
  15. deserializeRequest(request), request.type()); // 解析请求
  16. auto rrefsReadyFuture = rrefContext.waitForThreadLocalPendingRRefs();
  17. rrefsReadyFuture->addCallback(
  18. [this,
  19. retFuture,
  20. // std::function must be copyable, hence hae to cast the unique_ptr to
  21. // a shared_ptr here.
  22. rpc = (std::shared_ptr<RpcCommandBase>)std::move(rpc),
  23. messageType = request.type(),
  24. id = request.id(),
  25. ctx = std::move(ctx)](JitFuture& /* unused */) mutable {
  26. c10::MultiStreamGuard guard(
  27. ctx ? ctx->getReservedStreams() : ArrayRef<Stream>({}));
  28. // The cost of pre-request check is minimal thanks to
  29. // std::shared_lock. The cost is in magnitude
  30. // of 10us.
  31. auto serverProcessGlobalProfilerStateStackEntryPtr =
  32. profiler::processglobal::StateStackEntry::current();
  33. // If server global profiler is enabled, we futher pay the
  34. // cost of thread local profiler state initialization.
  35. if (serverProcessGlobalProfilerStateStackEntryPtr) {
  36. // Initialize thread-local profiler state from process-global
  37. // profiler state.
  38. ::torch::autograd::profiler::enableProfilerLegacy(
  39. serverProcessGlobalProfilerStateStackEntryPtr->statePtr()
  40. ->config());
  41. }
  42. // 在这里
  43. processRpcWithErrors(
  44. *rpc, messageType, id, retFuture, std::move(ctx));
  45. // Response message has been sent at this moment, this post-response
  46. // work doesn't affect RPC trip time.
  47. if (serverProcessGlobalProfilerStateStackEntryPtr) {
  48. // Restore thread-local profiler state.
  49. ::torch::autograd::profiler::thread_event_lists event_lists =
  50. ::torch::autograd::profiler::disableProfilerLegacy();
  51. // Put thread_local event_lists into the process-global profiler
  52. // state.
  53. profiler::processglobal::pushResultRecursive(
  54. serverProcessGlobalProfilerStateStackEntryPtr, event_lists);
  55. }
  56. });
  57. } catch (std::exception& e) {
  58. retFuture->markCompleted(handleError(e, request.type(), request.id()));
  59. rrefContext.clearRecordedPendingRRefsOnError();
  60. }
  61. return retFuture;
  62. }

然后调用到 processRpcWithErrors。

  1. void RequestCallbackNoPython::processRpcWithErrors(
  2. RpcCommandBase& rpc,
  3. const MessageType& messageType,
  4. const int64_t messageId,
  5. const c10::intrusive_ptr<JitFuture>& responseFuture,
  6. std::shared_ptr<LazyStreamContext> ctx) const {
  7. try {
  8. processRpc(rpc, messageType, messageId, responseFuture, std::move(ctx));
  9. } catch (std::exception& e) {
  10. responseFuture->markCompleted(handleError(e, messageType, messageId));
  11. }
  12. }

接下来是 processRpc。这里能够看到处理 FORWARD_AUTOGRAD_REQ。

  1. void RequestCallbackNoPython::processRpc(
  2. RpcCommandBase& rpc,
  3. const MessageType& messageType,
  4. const int64_t messageId,
  5. const c10::intrusive_ptr<JitFuture>& responseFuture,
  6. std::shared_ptr<LazyStreamContext> ctx) const {
  7. case MessageType::FORWARD_AUTOGRAD_REQ: { // 这里就和之前发送的对应上了
  8. processForwardAutogradReq(rpc, messageId, responseFuture, std::move(ctx));
  9. return;
  10. }
  11. case MessageType::BACKWARD_AUTOGRAD_REQ: {
  12. processBackwardAutogradReq(rpc, messageId, responseFuture);
  13. return;
  14. };
  15. }

具体如下:

  1. TensorPipeAgent RequestCallback RequestCallbackNoPython RequestCallbackImpl
  2. + + + +
  3. | | | |
  4. | | | |
  5. v | | |
  6. respond | | |
  7. + | | |
  8. | | | |
  9. | | | |
  10. v v v |
  11. cb_->operator() +--> operator() +--> processMessage |
  12. + |
  13. | |
  14. | v
  15. +---------------> deserializePythonRpcCommand
  16. |
  17. |
  18. |
  19. v
  20. processRpcWithErrors
  21. +
  22. |
  23. |
  24. v
  25. processRpc
  26. +
  27. |
  28. |
  29. v
  30. processForwardAutogradReq

4.3 RequestCallbackImpl

这时候,读者会有疑问,之前 TensorPipeAgent 明明设置了 RequestCallbackImpl 作为回调函数,怎么只调用了其 deserializePythonRpcCommand呢,deserialXXX 看起来是序列化相关的,按说应该调用一些业务处理函数,比如processXXXX 之类的。我们接下来就看看 RequestCallbackImpl。

RequestCallbackImpl 定义在 torch/csrc/distributed/rpc/request_callback_impl.h。

  1. class TORCH_API RequestCallbackImpl : public RequestCallbackNoPython {
  2. public:
  3. std::unique_ptr<RpcCommandBase> deserializePythonRpcCommand(
  4. std::unique_ptr<RpcCommandBase> rpc,
  5. const MessageType& messageType) const override;
  6. void processPythonCall(
  7. RpcCommandBase& rpc,
  8. const std::function<void(Message)>& markComplete,
  9. const int64_t messageId,
  10. const c10::intrusive_ptr<JitFuture>& responseFuture) const override;
  11. void processScriptCall(
  12. RpcCommandBase& rpc,
  13. const std::function<void(Message)>& markComplete,
  14. const int64_t messageId,
  15. const c10::intrusive_ptr<JitFuture>& responseFuture) const override;
  16. void processScriptRemoteCall(
  17. ScriptRemoteCall& scriptRemoteCall,
  18. const std::function<void(void)>& postProcessing,
  19. std::vector<at::IValue>& stack,
  20. const c10::intrusive_ptr<OwnerRRef>& ownerRRef) const override;
  21. void processPythonRemoteCall(
  22. RpcCommandBase& rpc,
  23. const std::function<void(Message)>& markComplete,
  24. const int64_t messageId,
  25. const c10::intrusive_ptr<JitFuture>& responseFuture,
  26. std::shared_ptr<LazyStreamContext> ctx) const override;
  27. void processRpcWithErrors(
  28. RpcCommandBase& rpc,
  29. const MessageType& messageType,
  30. const int64_t messageId,
  31. const c10::intrusive_ptr<JitFuture>& responseFuture,
  32. std::shared_ptr<LazyStreamContext> ctx) const override;
  33. void processRRefBackward(
  34. RpcCommandBase& rpc,
  35. const int64_t messageId,
  36. const c10::intrusive_ptr<JitFuture>& responseFuture) const override;
  37. };

因为最终生成的是 RequestCallbackImpl,所以实际上,上图中间有一步 processRpcWithErrors 实际调用的是 RequestCallbackImpl 这里的函数 processRpcWithErrors,其就是增加了一些异常处理逻辑。

  1. void RequestCallbackImpl::processRpcWithErrors(
  2. RpcCommandBase& rpc,
  3. const MessageType& messageType,
  4. const int64_t messageId,
  5. const c10::intrusive_ptr<JitFuture>& responseFuture,
  6. std::shared_ptr<LazyStreamContext> ctx) const {
  7. try {
  8. processRpc(rpc, messageType, messageId, responseFuture, std::move(ctx));
  9. } catch (py::error_already_set& e) {
  10. responseFuture->markCompleted(handleError(e, messageType, messageId));
  11. py::gil_scoped_acquire acquire;
  12. e.restore(); // Release ownership on py::objects and also restore
  13. // Python Error Indicator.
  14. PyErr_Clear(); // Clear the Python Error Indicator as we has
  15. // recorded the exception in the response message.
  16. } catch (std::exception& e) {
  17. responseFuture->markCompleted(handleError(e, messageType, messageId));
  18. }
  19. }

逻辑图修改如下:

  1. TensorPipeAgent RequestCallback RequestCallbackNoPython RequestCallbackImpl
  2. + + + +
  3. | | | |
  4. | | | |
  5. v | | |
  6. respond | | |
  7. + | | |
  8. | | | |
  9. | | | |
  10. v v v |
  11. cb_->operator() +--> operator() +--> processMessage |
  12. + |
  13. | |
  14. | v
  15. +----------------> deserializePythonRpcCommand
  16. | +
  17. | |
  18. | |
  19. | v
  20. |
  21. +----------------> processRpcWithErrors
  22. | +
  23. | |
  24. | |
  25. | <------------------------+
  26. |
  27. |
  28. v
  29. processRpc
  30. +
  31. |
  32. |
  33. v
  34. processForwardAutogradReq

如果结合之前的发送,我们拓展图例如下:

  1. 当发送者需要在远端运行自动梯度计算时候,调用 rpc.rpc_sync。
  2. 从 Python 调用到 C++ 世界,函数为 pyRpcBuiltin。
  3. 调用 sendMessageWithAutograd,以此通知Receiver。
  4. 会调用 RpcAgent::getCurrentRpcAgent() 来得到本地的 Agent。
  5. 调用 current Agent 的 send 函数。
  6. send 函数发送 FORWARD_AUTOGRAD_REQ给 Receiver worker。
  7. respond 函数会调用 Receiver 之中 Agent 的回调函数 cb_。
  8. 调用到 RequestCallbackImpl 的 processRpcWithErrors。
  9. 然后调用 processRpc。
  10. 最后调用到 processForwardAutogradReq,完成了基于RPC的分布式autograd的启动过程。
  1. +
  2. rpc.rpc_sync Sender | Receiver
  3. + |
  4. | |
  5. | 1 |
  6. v |
  7. _invoke_rpc_builtin |
  8. + |
  9. | Python |
  10. +----------------------------------------------------------+ |
  11. | C++ | +----------------------------+
  12. | 2 | | RequestCallbackImpl |
  13. v | | |
  14. | +----> processRpcWithErrors |
  15. pyRpcBuiltin | | | + |
  16. + | | | | 9 |
  17. | 3 | | | | |
  18. | | | | v |
  19. v | | | processRpc |
  20. 4 | | | + |
  21. sendMessageWithAutograd(RpcAgent::getCurrentRpcAgent()) | | | | 10 |
  22. + | | | | |
  23. | | | | v |
  24. | | | | processForwardAutogradReq |
  25. | RpcAgent::currentRpcAgent_ | | | |
  26. | + | | +----------------------------+
  27. | | | |
  28. | 5 | | |8 +-----------------+
  29. | v | | | TensorPipeAgent |
  30. | +------+--------+ | | | |
  31. | |TensorPipeAgent| +-------------------+ | +------------+ cb_ |
  32. | | | |RequestCallbackImpl| | | ^ |
  33. | | cb_ +------->+ | | | 7 | |
  34. | | | +-------------------+ | | | |
  35. | | | 6 | | + |
  36. +--------> send +----------------------------------+--------------> respond |
  37. | | FORWARD_AUTOGRAD_REQ | |
  38. | | + | |
  39. +---------------+ | +-----------------+
  40. +

手机如下:

至此,RPC介绍完毕,我们下一篇介绍上下文相关等管理类,敬请期待。

0xFF 参考

[源码解析] PyTorch 分布式 Autograd (2) ---- RPC基础的更多相关文章

  1. [源码解析] PyTorch 分布式 Autograd (3) ---- 上下文相关

    [源码解析] PyTorch 分布式 Autograd (3) ---- 上下文相关 0x00 摘要 我们已经知道 dist.autograd 如何发送和接受消息,本文再来看看如何其他支撑部分,就是如 ...

  2. [源码解析] PyTorch 分布式 Autograd (4) ---- 如何切入引擎

    [源码解析] PyTorch 分布式 Autograd (4) ---- 如何切入引擎 目录 [源码解析] PyTorch 分布式 Autograd (4) ---- 如何切入引擎 0x00 摘要 0 ...

  3. [源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上)

    [源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上) 目录 [源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上) 0x00 摘要 0x0 ...

  4. [源码解析] PyTorch 分布式 Autograd (6) ---- 引擎(下)

    [源码解析] PyTtorch 分布式 Autograd (6) ---- 引擎(下) 目录 [源码解析] PyTtorch 分布式 Autograd (6) ---- 引擎(下) 0x00 摘要 0 ...

  5. [源码解析] PyTorch 分布式(18) --- 使用 RPC 的分布式管道并行

    [源码解析] PyTorch 分布式(18) --- 使用 RPC 的分布式管道并行 目录 [源码解析] PyTorch 分布式(18) --- 使用 RPC 的分布式管道并行 0x00 摘要 0x0 ...

  6. [源码解析] PyTorch 分布式 Autograd (1) ---- 设计

    [源码解析] PyTorch 分布式 Autograd (1) ---- 设计 目录 [源码解析] PyTorch 分布式 Autograd (1) ---- 设计 0x00 摘要 0x01 分布式R ...

  7. [源码解析] PyTorch 分布式(14) --使用 Distributed Autograd 和 Distributed Optimizer

    [源码解析] PyTorch 分布式(14) --使用 Distributed Autograd 和 Distributed Optimizer 目录 [源码解析] PyTorch 分布式(14) - ...

  8. [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器

    [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 目录 [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 0x0 ...

  9. [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC

    [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC 目录 [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC 0x00 摘要 0x0 ...

随机推荐

  1. 免费 CDN 玩法 —— 文件一键上传到 NPM

    前言 unpkg.jsdelivr 等站点可加速 NPM 包文件,适合作为个人网站或演示案例的免费 CDN. 虽然上传文件到 NPM 很简单,创建 package.json 然后 npm publis ...

  2. 初学python-day3 元组

    day2 列表已更新!

  3. Promise.resolve(x)中x有几种情况

    ps:下面参数说的是Promise.resolve(x)中的x 一共四种情况: 1.如果参数是Promise实例本身,则抛出错误 2.如果参数是一个promise对象,则then函数的执行取决于这个参 ...

  4. 在Windows上使用Docker 创建MongoDB 副本集的极简方法(翻译)

    这篇博客介绍下在Windows上使用Docker 创建MongoDB 三节点副本集的最简单的方法.以下命令需要Docker for Windows并使用Linux 容器. 1: 为每个节点创建数据卷 ...

  5. 【Java虚拟机4】Java内存模型(硬件层面的并发优化基础知识--缓存一致性问题)

    前言 今天学习了Java内存模型第一课的视频,讲了硬件层面的知识,还是和大学时一样,醍醐灌顶.老师讲得太好了. Java内存模型,感觉以前学得比较抽象.很繁杂,抽象. 这次试着系统一点跟着2个老师学习 ...

  6. 【SDOI2014】数数(补)

    见 AC自动机(补坑了) [SDOI2014] 数数 简要题意:  我们称一个正整数N是幸运数,当且仅当它的十进制表示中不包含数字串集合S中任意一个元素作为子串.例如当S={22,333,0233}时 ...

  7. vs编译问题总结

    编译遇到MSIL .netmodule or module compiled with /GL found; restarting link with /LTCG; add /LTCG to the ...

  8. coreseek使用心得

    基本使用方法: D:\coreseek-4.1\bin\searchd -c D:\coreseek-4.1\etc\article.conf --stop 停止服务 D:\coreseek-4.1\ ...

  9. openstack 后期维护(四)--- 删除僵尸卷

    前言: 在长时间使用openstack之后,删除虚机后,经常会有因这样那样的问题,导致卷处于僵尸状态,无法删除! 状态一: 虚机已近删除,然而卷却挂在到了 None上无法删除 解决办法: 1.# ci ...

  10. 2020 ICPC 沈阳站 I - Rise of Shadows 题解

    题面看这里 \(PS\):符号 \([\ \rm P\ ]\) 的意义是:当表达式 \(\rm P\) 为真则取值为 \(1\),为假则取值为 \(0\). 题目大意 给你一个一天有 \(H\)​​​ ...