<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://vamix.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vamix.github.io/" rel="alternate" type="text/html" /><updated>2025-11-12T05:49:02-08:00</updated><id>https://vamix.github.io/feed.xml</id><title type="html">Guodong Liu</title><subtitle>personal website</subtitle><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><entry><title type="html">NCCL源码阅读笔记</title><link href="https://vamix.github.io/posts/2021/03/nccl/" rel="alternate" type="text/html" title="NCCL源码阅读笔记" /><published>2021-03-21T00:00:00-07:00</published><updated>2021-03-21T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2021/03/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2021/03/nccl/"><![CDATA[<p>最近简单读了一下NVIDIA Collective Communications Library (NCCL, pronounced “Nickel”) 的源代码。现将初步的一些收获总结一下。</p>

<!--more-->

<h2 id="代码框架">代码框架</h2>

<pre><code class="language-C++">nccl-master-src
├── Makefile
├── include
├── collectives 		// 集合通信原语的实现
├── graph						// 检测网络拓扑结构
├── transport				// 与数据传输相关的函数实现
├── bootstrap.cc		// （我的理解是）很多内建的helper function
├── channel.cc
├── debug.cc
├── enqueue.cc			// 关于队列的操作
├── group.cc				// NCCL group API的实现
├── init.cc					// 初始化的代码
├── proxy.cc				// Proxy线程相关的代码
└── transport.cc		// 数据传输相关

</code></pre>

<p>目前我读到的一些代码以及相应的功能已经标注在文件树中。关于底层的数据传输、怎样建立socket通信的代码，主要集中在<code class="language-plaintext highlighter-rouge">transport.cc</code> <code class="language-plaintext highlighter-rouge">proxy.cc</code>以及<code class="language-plaintext highlighter-rouge">transport</code>文件夹中。关于上层的集合通信原语的实现，例如Allreduce, Allgather等，主要集中在<code class="language-plaintext highlighter-rouge">collectives</code>文件夹中。</p>

<h2 id="start-from-nccl-example">Start from NCCL example</h2>

<p>拿到源码有点无从下手，先看一个NCCL Doc中给出的example。根据example调用的API入手阅读源码。</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[])</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">size</span> <span class="o">=</span> <span class="mi">32</span><span class="o">*</span><span class="mi">1024</span><span class="o">*</span><span class="mi">1024</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">myRank</span><span class="p">,</span> <span class="n">nRanks</span><span class="p">,</span> <span class="n">localRank</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

  <span class="c1">//initializing MPI</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">argc</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">argv</span><span class="p">));</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Comm_rank</span><span class="p">(</span><span class="n">MPI_COMM_WORLD</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">myRank</span><span class="p">));</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Comm_size</span><span class="p">(</span><span class="n">MPI_COMM_WORLD</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">nRanks</span><span class="p">));</span>

  <span class="c1">//calculating localRank based on hostname which is used in selecting a GPU</span>
  <span class="kt">uint64_t</span> <span class="n">hostHashs</span><span class="p">[</span><span class="n">nRanks</span><span class="p">];</span>
  <span class="kt">char</span> <span class="n">hostname</span><span class="p">[</span><span class="mi">1024</span><span class="p">];</span>
  <span class="n">getHostName</span><span class="p">(</span><span class="n">hostname</span><span class="p">,</span> <span class="mi">1024</span><span class="p">);</span>
  <span class="n">hostHashs</span><span class="p">[</span><span class="n">myRank</span><span class="p">]</span> <span class="o">=</span> <span class="n">getHostHash</span><span class="p">(</span><span class="n">hostname</span><span class="p">);</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Allgather</span><span class="p">(</span><span class="n">MPI_IN_PLACE</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">MPI_DATATYPE_NULL</span><span class="p">,</span> <span class="n">hostHashs</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint64_t</span><span class="p">),</span> <span class="n">MPI_BYTE</span><span class="p">,</span> <span class="n">MPI_COMM_WORLD</span><span class="p">));</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">p</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">p</span><span class="o">&lt;</span><span class="n">nRanks</span><span class="p">;</span> <span class="n">p</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
     <span class="k">if</span> <span class="p">(</span><span class="n">p</span> <span class="o">==</span> <span class="n">myRank</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
     <span class="k">if</span> <span class="p">(</span><span class="n">hostHashs</span><span class="p">[</span><span class="n">p</span><span class="p">]</span> <span class="o">==</span> <span class="n">hostHashs</span><span class="p">[</span><span class="n">myRank</span><span class="p">])</span> <span class="n">localRank</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="n">ncclUniqueId</span> <span class="n">id</span><span class="p">;</span>
  <span class="n">ncclComm_t</span> <span class="n">comm</span><span class="p">;</span>
  <span class="kt">float</span> <span class="o">*</span><span class="n">sendbuff</span><span class="p">,</span> <span class="o">*</span><span class="n">recvbuff</span><span class="p">;</span>
  <span class="n">cudaStream_t</span> <span class="n">s</span><span class="p">;</span>

  <span class="c1">//get NCCL unique ID at rank 0 and broadcast it to all others</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">myRank</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">ncclGetUniqueId</span><span class="p">(</span><span class="o">&amp;</span><span class="n">id</span><span class="p">);</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Bcast</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">id</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">id</span><span class="p">),</span> <span class="n">MPI_BYTE</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">MPI_COMM_WORLD</span><span class="p">));</span>

  <span class="c1">//picking a GPU based on localRank, allocate device buffers</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaSetDevice</span><span class="p">(</span><span class="n">localRank</span><span class="p">));</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaMalloc</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sendbuff</span><span class="p">,</span> <span class="n">size</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)));</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaMalloc</span><span class="p">(</span><span class="o">&amp;</span><span class="n">recvbuff</span><span class="p">,</span> <span class="n">size</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">float</span><span class="p">)));</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaStreamCreate</span><span class="p">(</span><span class="o">&amp;</span><span class="n">s</span><span class="p">));</span>

  <span class="c1">//initializing NCCL</span>
  <span class="n">NCCLCHECK</span><span class="p">(</span><span class="n">ncclCommInitRank</span><span class="p">(</span><span class="o">&amp;</span><span class="n">comm</span><span class="p">,</span> <span class="n">nRanks</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="n">myRank</span><span class="p">));</span>

  <span class="c1">//communicating using NCCL</span>
  <span class="n">NCCLCHECK</span><span class="p">(</span><span class="n">ncclAllReduce</span><span class="p">((</span><span class="k">const</span> <span class="kt">void</span><span class="o">*</span><span class="p">)</span><span class="n">sendbuff</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span><span class="o">*</span><span class="p">)</span><span class="n">recvbuff</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">ncclFloat</span><span class="p">,</span> <span class="n">ncclSum</span><span class="p">,</span>
        <span class="n">comm</span><span class="p">,</span> <span class="n">s</span><span class="p">));</span>

  <span class="c1">//completing NCCL operation by synchronizing on the CUDA stream</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaStreamSynchronize</span><span class="p">(</span><span class="n">s</span><span class="p">));</span>

  <span class="c1">//free device buffers</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaFree</span><span class="p">(</span><span class="n">sendbuff</span><span class="p">));</span>
  <span class="n">CUDACHECK</span><span class="p">(</span><span class="n">cudaFree</span><span class="p">(</span><span class="n">recvbuff</span><span class="p">));</span>

  <span class="c1">//finalizing NCCL</span>
  <span class="n">ncclCommDestroy</span><span class="p">(</span><span class="n">comm</span><span class="p">);</span>

  <span class="c1">//finalizing MPI</span>
  <span class="n">MPICHECK</span><span class="p">(</span><span class="n">MPI_Finalize</span><span class="p">());</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"[MPI Rank %d] Success </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">myRank</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>可以看到，这段代码先是执行了MPI相关的一些操作。调用MPI主要是为了在NCCL连接还未建立时，在设备之间传输一些额外信息，比如<code class="language-plaintext highlighter-rouge">ncclUniqueId</code>。然后是CUDA相关的一些操作，分配内存、创建stream等等。真正涉及到NCCL的代码，在上述example中只有两个API：<code class="language-plaintext highlighter-rouge">ncclCommInitRank</code>，<code class="language-plaintext highlighter-rouge">ncclAllReduce</code>。（除去<code class="language-plaintext highlighter-rouge">ncclUniqueId</code>和<code class="language-plaintext highlighter-rouge">ncclCommDestroy</code>）。</p>

<p>因此，我们就来看看这两个API是如何调用的好了。</p>

<h2 id="初始化communicator-ncclcomminitrank">初始化Communicator： <code class="language-plaintext highlighter-rouge">ncclCommInitRank</code></h2>

<pre><code class="language-C++">ncclResult_t ncclCommInitRank(ncclComm_t  comm, int nranks, ncclUniqueId commId, int rank)
</code></pre>

<p>要使用NCCL进行通信，每个设备上都要有一个NCCL Communicator object。属于同一个Communicator的各个设备具有相同的<code class="language-plaintext highlighter-rouge">ncclUniqueId</code>以及不同的<code class="language-plaintext highlighter-rouge">rank</code>。这个API用于在一个设备上初始化Communicator object。在设置不同设备上的communicator时，这个API必须被不同的线程/进程调用。或者使用<code class="language-plaintext highlighter-rouge">ncclGroupStart/ncclGroupEnd</code> 来通过一个线程/进程设置多个设备的Communicator。</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">(init.cc) ncclCommInitRank</code>-&gt; ` (init.cc) ncclCommInitRankDev`</p>

    <pre><code class="language-C++">ncclCommInitRankDev(){
	// rank == 0 时,执行bootstrapCreateRoot
	if (env &amp;&amp; myrank == 0)
    bootstrapCreateRoot();
	// 每个rank都要执行
	ncclCommInitRankSync();
}
</code></pre>
  </li>
  <li>
    <p>` (init.cc) ncclCommInitRankDev<code class="language-plaintext highlighter-rouge"> -&gt;</code>bootstrapCreateRoot`</p>

    <pre><code class="language-C++">bootstrapCreateRoot(){
  // 此处用来create listen socket的Addr是Communicator 的UniqID
  createListenSocket(&amp;listenFd, connectAddr);
  // 创建监听线程
  pthread_create(&amp;thread, NULL, bootstrapRoot, (void*)(uint64_t)listenFd);
}
</code></pre>
  </li>
  <li>
    <p>` (init.cc) ncclCommInitRankDev<code class="language-plaintext highlighter-rouge"> -&gt;</code>ncclCommInitRankSync`</p>

    <pre><code class="language-C++">ncclResult_t ncclCommInitRankSync(ncclComm_t* newcomm, int nranks, ncclUniqueId commId, int myrank, int cudaDev) {
  initTransportsRank(*newcomm, &amp;commId);
  devCommSetup(*newcomm);
}
</code></pre>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">initTransportsRank</code> 这个函数对数据传输做了大量的初始化工作。包括：</p>

    <ul>
      <li>
        <p>建立设备之间的socket连接</p>
      </li>
      <li>
        <p>检测系统里的设备以及设备之间的拓扑结构</p>
      </li>
      <li>
        <p>计算当前系统中的RING、TREE、COLLNET结构</p>

        <blockquote>
          <p>CollNet is a new algorithm in NCCL that allows GPUs on multiple nodes to do in-network reductions.</p>
        </blockquote>
      </li>
      <li>
        <p>建立每个设备之间的连接。peer之间的通信方式有三种：</p>

        <ul>
          <li>p2p transport (uses CUDA direct access between GPUs, using NVLink or PCI.)</li>
          <li>shared memory transport (using host memory.)</li>
          <li>net transport (InfiniBand or IP sockets.)</li>
        </ul>

        <p>在<code class="language-plaintext highlighter-rouge">ncclTransportP2pSetup() -&gt; selectTransport()</code>中，会选择各个peer之间适用的通信方式。对于p2p和shm通信方式，在建立连接后，可以直接进行数据传输（通过GPU peer-to-peer或者host memory），而通过network连接的peer，还需要proxy线程来通过socket进行数据传输。</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="集合通信原语的实现">集合通信原语的实现</h2>

<p>在完成设备的Communicator初始化后，就可以调用集合通信的相关原语。在这里我们以Allreduce为例，分析集合通信原语的实现逻辑。</p>

<pre><code class="language-C++">NCCL_API(ncclResult_t, ncclAllReduce, const void* sendbuff, void* recvbuff, size_t count,
    ncclDataType_t datatype, ncclRedOp_t op, ncclComm* comm, cudaStream_t stream);
ncclResult_t ncclAllReduce(const void* sendbuff, void* recvbuff, size_t count,
    ncclDataType_t datatype, ncclRedOp_t op, ncclComm* comm, cudaStream_t stream) {
  NVTX3_FUNC_RANGE_IN(nccl_domain);
  struct ncclInfo info = { ncclFuncAllReduce, "AllReduce",
    sendbuff, recvbuff, count, datatype, op, 0, comm, stream, /* Args */
    ALLREDUCE_CHUNKSTEPS, ALLREDUCE_SLICESTEPS };
  return ncclEnqueueCheck(&amp;info);
}
</code></pre>

<p>可以看到，调用<code class="language-plaintext highlighter-rouge">ncclAllReduce</code>时，所有的变量被存到一个<code class="language-plaintext highlighter-rouge">ncclInfo</code> 结构体中，然后通过<code class="language-plaintext highlighter-rouge">ncclEnqueueCheck</code> 将这个结构体插入到队列中。</p>

<p><strong><code class="language-plaintext highlighter-rouge">ncclEnqueueCheck</code></strong> 执行了以下操作：</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ncclSaveKernel </code>：</p>

    <p>对将要执行的操作进行一些准备工作。主要是调用<code class="language-plaintext highlighter-rouge">computeColl()</code>计算<code class="language-plaintext highlighter-rouge">ncclProxyArgs</code> 这个结构体中的信息。这个结构体将被用于初始化Proxy。并且这个函数也计算了将要launch的 CUDA kernel的参数。</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ncclBarrierEnqueue</code> <code class="language-plaintext highlighter-rouge">ncclBarrierEnqueueWait</code>：</p>

    <p>launch所有的 CUDA kernel，通过插入barrier的方式设置kernel之间的依赖关系。最后，通过<code class="language-plaintext highlighter-rouge">ncclProxyStart()</code> 启动Proxy线程。</p>
  </li>
</ul>

<p>AllReduce的具体执行逻辑：<code class="language-plaintext highlighter-rouge">all_reduce.h</code> 中，有9种实现。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>三种算法：NCCL_ALGO_RING, NCCL_ALGO_TREE, NCCL_ALGO_COLLNET
三种协议：NCCL_PROTO_SIMPLE, NCCL_PROTO_LL, NCCL_PROTO_LL128
</code></pre></div></div>

<pre><code class="language-C++">// 以NCCL_ALGO_RING, NCCL_PROTO_SIMPLE的实现为例
// 首先从args这个结构体中获取当前操作所需要的参数，例如当前的channel，数据传输的chunk size等。
// 实例化一个ncclPrimitives类
ncclPrimitives&lt;UNROLL, ALLREDUCE_CHUNKSTEPS/ALLREDUCE_SLICESTEPS, ALLREDUCE_SLICESTEPS, T, 1, 1, 1, FUNC&gt; prims(tid, nthreads, &amp;ring-&gt;prev, &amp;ring-&gt;next, thisOutput, stepSize, channel, comm, ncclShmem-&gt;ptrs, 0);
</code></pre>

<ul>
  <li>
    <p>一个非常重要的类：<code class="language-plaintext highlighter-rouge">ncclPrimitives</code> 。这个类实现了各类通信原语。</p>

    <pre><code class="language-C++">#define SYNC_GROUP 8
static_assert(NSEND &lt; SYNC_GROUP &amp;&amp; NRECV &lt; SYNC_GROUP, "Not enough threads to cover all peers");
  
int g = tid / SYNC_GROUP;
int ng = nthreads / SYNC_GROUP;
index = tid % SYNC_GROUP;
  
if (g == 0) {
if (index &lt; nrecv) role |= ROLE_WAIT_RECV;
if (index == nrecv) role |= ROLE_SRC;
} else if (g == 1) {
if (index &lt; nsend) role |= ROLE_WAIT_SEND;
if (index == nsend) role |= ROLE_DST;
} else if (g == ng - 2) {
if (index &lt; nrecv) role |= ROLE_POST_RECV;
} else if (g == ng - 1) {
if (index &lt; nsend) role |= ROLE_POST_SEND;
}
</code></pre>

    <p>根据<code class="language-plaintext highlighter-rouge">tid（thread ID）</code>、<code class="language-plaintext highlighter-rouge">nthreads</code>  以及<code class="language-plaintext highlighter-rouge">SYNC_GROUP</code> 就能确定当前设备的角色。这些角色分别是什么意思？</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ROLE_WAIT_RECV
ROLE_SRC
ROLE_WAIT_SEND
ROLE_DST
ROLE_POST_RECV
ROLE_POST_SEND
</code></pre></div>    </div>

    <p>比较巧妙的设计是，通过设计一个GenericOp，改变其6个input的值就能使用相同的代码实现多种不同的功能。</p>

    <pre><code class="language-C++">template &lt;int DIRECTRECV, int DIRECTSEND, int RECV, int SEND, int SRC, int DST&gt;
inline __device__ void
GenericOp(const T* srcPtr, T* dstPtr, int nelem, ssize_t directOffset) 
</code></pre>

    <p>在GenericOp中，调用send和recv之类的操作时，会修改<code class="language-plaintext highlighter-rouge">ncclConnInfo</code> 这个结构体中的<code class="language-plaintext highlighter-rouge">void* *ptrsFifo; </code> 以及<code class="language-plaintext highlighter-rouge">int *sizesFifo; </code> 这两个数据结构，这两个结构中存储着要传输的数据地址以及对应的数据大小。在 netTransport的proxy相关的函数中，会检查这个队列，进行传输。</p>
  </li>
</ul>

<h2 id="其他">其他</h2>

<ul>
  <li>
    <p>异步模式是如何被启用的？</p>

    <pre><code class="language-C++">bool ncclAsyncMode() {
  return ncclGroupMode &gt; 0;
}
</code></pre>

    <p>可以看到，异步模式仅在 <code class="language-plaintext highlighter-rouge">ncclGroupMode&gt;0</code>时启用。而这个变量是被group操作相关的函数所修改的。</p>

    <pre><code class="language-C++">ncclResult_t ncclGroupStart() {
  NVTX3_FUNC_RANGE_IN(nccl_domain);
  if (ncclGroupMode == 0) {
    memset(ncclGroupArgs, 0, sizeof(struct ncclAsyncArgs)*MAX_ASYNC_OPS);
  }
  ncclGroupMode++;
  return ncclSuccess;
}
</code></pre>

    <p>可以看出，每次使用group操作时，都会启用异步模式。异步模式下，很多操作不再阻塞等待结果返回。因此同一个函数的同步实现和异步实现会有些许不同。</p>
  </li>
  <li>
    <p>关于NCCL call在GPU上执行的方式：</p>

    <blockquote>
      <p>The NCCL call returns when the operation has been effectively enqueued to the given stream, or returns an error. The collective operation is then executed asynchronously on the CUDA device.</p>
    </blockquote>

    <p>类似于TensorFlow中，各个operator的执行流程。先是enqueue到一个给定的stream。然后在GPU上异步地执行，The operation status can be queried using standard CUDA semantics, for example, calling <code class="language-plaintext highlighter-rouge">cudaStreamSynchronize</code> or using CUDA events。</p>
  </li>
  <li>
    <p>以broadcast为例分析集合通信原语的具体逻辑：</p>

    <pre><code class="language-C++">for (ssize_t gridOffset = 0; gridOffset &lt; size; gridOffset += loopSize) {
  int realChunkSize = min(chunkSize, DIVUP(size-gridOffset,nChannels));
  ALIGN_SIZE(realChunkSize, nthreads*sizeof(uint64_t)/sizeof(T));
  ssize_t offset = gridOffset + bid*realChunkSize;
  int nelem = min(realChunkSize, size-offset);
  
  if (rank == root) {
    // 如果是root，就要向其他设备广播当前设备buffer中的数据
    if (thisInput == thisOutput) {
      prims.send(thisInput+offset, nelem);
    } else {
      prims.copySend(thisInput+offset, thisOutput+offset, nelem);
    }
  }
  // 如果不是root，就要从其他节点接收root传来的数据。因为是ring结构，所以root发来的数据可能在环上依次传递，并不一定直接接收来自root的数据。
  else if (nextRank == root) {
    //如果是root之前的最后一个设备，接收即可，不再发送。
    prims.recv(thisOutput+offset, nelem);
  } else {
    // 否则，不仅要receive，还要copy,send.
    prims.recvCopySend(thisOutput+offset, nelem);
  }
}
</code></pre>
  </li>
  <li>
    <p>proxy可以看做是在CPU端运行了一个当前GPU跟其他GPU通信的代理，由proxy来决定通过何种方式传输数据。（采用netTransport的）Proxy会运行一个驻留线程。循环检查队列中有无op。op-&gt;progress则是每个op对应的proxy操作。</p>
  </li>
  <li>
    <p>跨CPU的数据通信可以采用Socket、Infiniband等机制。</p>
  </li>
  <li>
    <p>GPU上的kernel内部也有一个（待传输数据的）队列。这个队列是怎样维护的？</p>

    <p>这个队列不需要维护。在launch kernel之后，kernel会计算要传输的数据的地址和大小，修改<code class="language-plaintext highlighter-rouge">ncclConnInfo</code> 这个结构体中的数据队列指针<code class="language-plaintext highlighter-rouge">*tail</code>，然后调用同步函数，等待proxy线程传输刚刚添加到<code class="language-plaintext highlighter-rouge">*tail</code>中的数据。</p>

    <p>因此，数据传输过程其实是GPU上的NCCL kernel跟CPU上的Proxy线程协同完成的。</p>

    <p>Proxy线程中与数据传输有关的三个函数是</p>

    <p><code class="language-plaintext highlighter-rouge">ncclNetIrecv</code>：Proxy从网络收到数据</p>

    <p><code class="language-plaintext highlighter-rouge">ncclNetIsend</code>：Proxy发送数据到网络</p>

    <p><code class="language-plaintext highlighter-rouge">ncclNetIflush</code>: Proxy将数据从CPU传到GPU</p>

    <p>在NCCL中没有这三个函数的实现，应该是调用了外部API。</p>
  </li>
  <li>
    <p>数据怎样从一个GPU传输到同一个节点的另一个GPU？</p>

    <p>Peer-to-peer, PCI+host memory</p>
  </li>
  <li>
    <p>数据怎样从一个GPU传输到另一个节点的GPU？</p>

    <p>Socket, InfiniBand</p>
  </li>
</ul>

<h2 id="总结">总结</h2>

<p>总的来说，NCCL的工作流程如下：</p>

<ol>
  <li>
    <p>首先，每个要参与数据传输的GPU都要调用<strong><code class="language-plaintext highlighter-rouge">ncclCommInitRank</code></strong>创建一个与其<code class="language-plaintext highlighter-rouge">rank</code>对应的Communicator，同一个communication group中的每个communicator具有相同的unique ID。</p>
  </li>
  <li>
    <p>当每个设备调用<code class="language-plaintext highlighter-rouge">ncclCommInitRank</code>时，设备之间会交换一些信息，例如各自的IP，bus ID等。然后检测整个系统中的网络拓扑结构。</p>
  </li>
  <li>
    <p>有了网络拓扑结构，NCCL会进一步搜索当前网络中最佳的RING、TREE、COLLNET图结构。</p>
  </li>
  <li>
    <p>有了设备之间的图结构信息，就可以在存在通路的设备之间建立点对点的连接。主要有三种连接方式：p2p，shared memory以及network。采用哪种方式取决于这两个节点之间支持怎样的连接方式。</p>

    <p>以上就是初始化阶段的所有准备工作。</p>
  </li>
  <li>
    <p>初始化完成后，就可以调用集合通信原语。例如<code class="language-plaintext highlighter-rouge">ncclAllReduce</code>。集合通信函数会被enqueue到一个CUDA stream上，在GPU上异步执行。</p>
  </li>
  <li>
    <p>接下来在CPU上启动Proxy线程，作为GPU上集合通信kernel的代理，与GPU kernel协同完成与其他设备之间的数据传输。GPU kernel负责计算所需传输的数据的地址以及数据量大小，而Proxy线程负责完成实际的数据传输。对于采用p2pTransport 以及shmTransport的设备，在建立连接后可以直接传输数据，对于采用netTransport的设备，则需要通过socket进行数据传输。</p>
  </li>
</ol>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="NCCL" /><summary type="html"><![CDATA[最近简单读了一下NVIDIA Collective Communications Library (NCCL, pronounced “Nickel”) 的源代码。现将初步的一些收获总结一下。]]></summary></entry><entry><title type="html">What drives performance improvement of Deep Learning Models in recent years</title><link href="https://vamix.github.io/posts/2020/09/paper-reading/" rel="alternate" type="text/html" title="What drives performance improvement of Deep Learning Models in recent years" /><published>2020-09-01T00:00:00-07:00</published><updated>2020-09-01T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2020/09/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2020/09/paper-reading/"><![CDATA[<p>这一周主要是看论文，浏览了今年MLSys，ASPLOS以及去年SOSP和前年OSDI上与系统和机器学习相关的文章，重点阅读了一些与深度学习的性能提升相关的文章。受到之前包老师推荐阅读的文章（There’s plenty of room at the Top）的影响，我把这些文章分为三类，分别是Algorithm、Software和Hardware Architecture。由于我关注的这些会议都是偏系统的，所以Software层面的文章比较多。</p>

<!--more-->

<h2 id="algorithm">Algorithm</h2>

<p>在这几个会议中，我看到了两篇关于算法层面的文章，都出自MLSys 2020。</p>

<p>其中<strong>BPPSA (MLSys 2020)</strong>对神经网络反向计算梯度的过程中普遍采用的Back Propagation算法，进行了新的设计。利用一个现有的Blelloch scan algorithm算法，将梯度计算过程转化为scan操作，从而将BP算法的复杂度从O(n) 降到 O(log n)， 提高了反向计算梯度时的性能。</p>

<p><strong>SLIDE (MLSys 2020)</strong> 这篇文章的标题就很有意思： <em>In Defense of Smart Algorithms over Hardware Acceleration for Large-Scale Deep Learning Systems</em>。 这篇文章指出了现在深度学习领域的一个趋势，就是对于基础的矩阵乘法等操作的优化已经达到了瓶颈，而深度学习算法本身又没有太多的发展，导致大家都开始研究专用于深度学习加速的硬件，而研发硬件不仅成本巨大，还很有风险，因为一旦上层的算法发生改变，可能之前的硬件架构就不能实现很好的利用率。因此这篇文章仍然尝试从算法层面对深度学习的性能进行优化。这篇文章采用了Adaptive dropout + Locality Sensitive Hash (LSH) table（一种在梯度更新过程中动态进行dropout的操作），并结合了HOGWILD（一种异步更新的SGD算法），从而在一个44核CPU上实现了Tesla V100 GPU 3.5倍的性能。</p>

<h2 id="software">Software</h2>

<p>在软件层面对深度学习模型的性能提升，自底向上可以分为：operator-level, graph-level, task-level。Operator-level的提升，包含cuDNN这样的加速库，从算子的实现层面进行了加速，但这类的工作比较少；graph-level的工作就是对神经网络的机构进行优化，这类工作也比较少；最多的优化是在task-level进行的，这个task-level是我自己总结的，在这里，一个task可以认为是神经网络训练或者推理过程中的一次前向计算或者反向参数更新（对于某些对神经网络进行划分的工作，这个task的粒度要小一些），很多工作就是不去看神经网络的底层实现细节，从task-level来进行任务的调度和分布式部署。</p>

<p>对于operator-level，我在这几个会议中只看到了<strong>TVM(OSDI 2018)</strong> 这一篇文章，从编译器的角度，针对不同的op生成优化后的代码。而在graph-level，<strong>TVM</strong>同样也进行了一定程度的，对神经网络计算图的优化。除此之外，就是Jia Zhihao发表的<strong>TASO</strong> <strong>(SOSP 2019)</strong> ，通过设计了一些神经网络子图的替换规则，利用回溯搜索的算法，自动生成替换后的网络结构。</p>

<p>下面主要分析task-level的工作。</p>

<p>在task-level，我们说的对“神经网络性能的提升”，也包括好几个场景，分别是<strong>超参数调优过程</strong>、<strong>训练阶段</strong>和<strong>推理阶段</strong>。这几个场景具有各自不同的特点，因此需要针对性地进行系统的设计。</p>

<p><strong>超参数调优过程</strong>，会在很多组不同的神经网络参数甚至不同的神经网络结构下进行计算，评估模型的准确率，找出最优的参数组合，这其中运行的每一个任务的收敛性、模型结构和大小、资源占用情况可能都不相同，对于某些收敛性很差的参数组合，可能还需要提前终止来节省时间开销。</p>

<p><strong>训练阶段</strong>是在一组已经确定的参数组合下，迭代地进行大量重复运算，每一次迭代都会更新模型中的参数，由于神经网络算法内在的数据依赖性，导致不同迭代周期之间难以并行，而同一迭代周期内的数据并行又会引入大量时间开销。</p>

<p><strong>推理阶段</strong>虽然可以看做是一次训练阶段的前向计算过程，但推理往往发生在云服务器中，训练好的模型被用于处理大量的prediction query，这个场景需要关注的就不是运算性能，而是延迟、QoS等指标。同时，推理也会发生在各种移动设备端或者嵌入式设备中，在资源受限的情况下，如何保证低功耗和推理精度，也是大家比较关心的问题。</p>

<ul>
  <li>
    <p><strong>超参数调优</strong></p>

    <p>在MLSys 2020上有两篇关于超参数调优的文章，其中一篇（<strong>FLEET</strong>）将超参数调优问题归纳为<strong>ensemble training</strong>， 在ensemble training中，训练一组结构相同的网络，称为<em>homogeneous ensemble</em>， 否则称为<em>heterogeneous ensemble</em>。 <em>heterogeneous ensemble</em>存在两个挑战：不同网络的训练速率不同，这会导致整个集群需要等待较慢的任务完成后才能释放某些硬件资源，资源利用率较低；收敛速率不同，也会导致硬件利用率较低。因此作者将这个问题转化为一个<strong>最优资源分配问题</strong>，通过提出一个高效的贪心算法，以及对DNN任务进行分组，FLEET实现了高效的资源分配和数据传输。</p>
  </li>
  <li>
    <p><strong>训练阶段</strong></p>

    <p>对于训练阶段的性能提升，很多工作是对任务的划分和调度方式进行了设计。例如<strong>Generic Communication Scheduler (SOSP 2019)</strong> 通过对tensor进行细粒度的划分和按照<strong>优先级调度</strong>，从而提升了模型训练的性能。 <strong>PipeDream (SOSP 2019)</strong> 在模型并行的训练中引入了<strong>流水线</strong>机制，通过batch之间的并行，从而提高了硬件利用率，减少了训练时间。</p>

    <p>另一些工作则是从数据传输的角度来进行优化。目前广泛使用的数据并行在单机多卡的场景下，往往采用All-reduce算法来进行参数同步，All-reduce的后端往往采用NVIDIA提供的NCCL库。<strong>BLink (MLSys 2020) **针对NVLink场景中，NCCL库存在的缺陷进行优化，采用了<em>packing spanning trees</em>算法，从而进一步提高带宽利用率。</strong>Prague (ASPLOS 2020) <strong>针对异构环境下的分布式训练，提出了新的</strong>Partial All-reduce<strong>原语，避免了All-reduce存在的straggler问题，并且提出了新的分组和调度算法，对异构环境有更好的适应性。</strong>PLink (MLSys 2020)** 则主要关注云服务器端的分布式训练，主要包含三个模块：ProbeEmbed用于探索集群中的网络拓扑结构，AggEngine根据拓扑信息，对workloads进行聚集，生成balanced部署方案，Autotune用于在网络条件变化时动态调整。</p>

    <p>除了提升训练性能，很多工作也提高了集群硬件资源利用率。<strong>Gandiva (OSDI 2018)</strong> 是针对GPU设计的调度器，作者根据神经网络任务的资源占用周期性，对GPU进行时间分片，并且加入了高效的任务换出机制，让不同的任务周期性地使用GPU，从而提高GPU的利用率。<strong>Salus（MLSys 2020）</strong>针对现阶段GPU任务调度中存在的粒度过大的问题，提出了细粒度GPU共享原语：<em>fast job switching</em> and <em>memory sharing</em>， 前者可以实现高效的GPU时间共享和抢占，后者通过将大量小的DL任务打包，保证了内存的使用率。<strong>Resource Elasticity in Distributed Deep Learning (MLSys 2020)</strong> 指出传统的机器学习框架，例如TensorFlow建立在固定的资源分配基础之上，一个任务往往与一定数量的资源绑定，直到这个任务计算完成，这并不能实现灵活的资源分配。而分布式场景下固定的资源分配可能会导致较低的利用率，集群中可能存在的straggler也可能导致整体性能下降，因此需要动态调整资源的分配。这篇文章在Tensorflow之上实现了一个动态调整资源分配的模块，提高了总体资源利用率，解决了straggler的问题。</p>
  </li>
  <li>
    <p><strong>推理阶段</strong></p>

    <p>对于推理阶段，研究者主要关注大规模推理系统以及边缘计算设备中的推理性能。</p>

    <p>对于大规模推理系统，<strong>Parity model (SOSP 2019) **关注的是推理系统可能由多个分布式节点组成，系统可能遭受尾延迟的影响从而导致服务质量下降。作者提出用erasure code来对用户的query进行编码，从而通过很少的额外空间开销减少了尾延迟的影响。</strong>Willump (MLSys 2020)** 则指出目前的推理系统没有利用深度学习负载的特性，由于深度学习模型的计算存在模糊性，我们可以在推理系统中构造一个简单模型，只计算一部分feature，用于区分简单的数据；对于Top-K类型的query，可以先通过近似计算排除分数低的elements，再使用深度学习模型计算分数较高的elements，从而提升推理性能。PRETZEL (OSDI 2018) 指出目前的大规模推理系统主要关注易于部署的问题，通常把模型本身当做一个黑盒子部署到容器中来执行，在黑盒模型下，深度学习模型本身的流水线特性不能被利用，只能从caching, batching and buffering 角度来优化性能。</p>

    <p>对于嵌入式设备，<strong>MNN (MLSys 2020)</strong> 是一个移动设备的通用推理引擎，主要解决了移动端推理的两大挑战：通用性和效率。通用性包括要兼容来自不同深度学习框架训练出的模型，以及要兼容不同的硬件设备；在推理效率的前提下还要尽可能节省硬件资源。<strong>SkyNet (MLSys 2020)</strong> 是针对嵌入式设备的硬件资源限制，自下而上地设计的高效推理网络，用于嵌入式设备的目标检测。**Memory-Driven Mixed Low Precision Quantization (MLSys 2020) **按照不同的tensor进行不同精确度的量化，既满足嵌入式设备的内存限制，又最大程度保证模型精度。</p>
  </li>
</ul>

<h2 id="hardware-architeture">Hardware Architeture</h2>

<p>这几个会议上关于深度学习硬件体系结构的工作比较少，主要看到了以下3篇：<strong>DNNGuard (ASPLOS 2020) **是为了提高深度学习硬件的安全性，提出的一种新的硬件架构，能够在运行原始模型的同时，运行另一个攻击检测模型，在保证运算性能的同时，提高深度学习模型的安全性。</strong>OPTIMUS (MLSys 2020)<strong>是专门为了加速Transformer 模型（一个常用于翻译的循环神经网络）的推理而提出的硬件结构。作者通过分析算法原理，跳过冗余计算，提高MAC(乘加器)的利用率。</strong>PoET-BiN (MLSys 2020)** 是在FPGA上利用LUT结构搭建二元神经网络。</p>

<p>在浏览这几个会议的文章时，虽然我主要关注性能相关的文章，不过也注意到了一些比较有意思的其他主题：</p>

<ul>
  <li>
    <p><strong>一个趋势：内存很有限</strong></p>

    <p>随着神经网络研究的深入，人们开始训练高精度图像、超长的语言序列、3D点云数据等等。这些训练数据往往会占用大量的内存。内存占用主要是由网络各个layer的中间结果（intermediate activations）造成的，因为这些中间结果会被用于计算梯度值，所以会在一次迭代周期的整个过程中被存在内存中。（这也限制了PipeDream没办法进一步提高硬件利用率）。</p>

    <p>GPU上的有限内存使得人们不能在这样的数据上采用复杂的网络结构或者较大的batch size。于是出现了一种解决方案，就是tensor rematerialization。可以在前向计算过程中释放中间结果，在计算梯度需要用到时，再通过一定的计算把这些中间结果重新算出来。而这会引入额外的计算时间。<strong>Checkmate (MLSys 2020)</strong>，通过一种近似算法寻找最优的tensor rematerialization方案，从而最小化总体的额外运算时间。</p>

    <p>图神经网络(GNN)处理的单个数据也比较大，使其难以进行大规模的训练和推理。<strong>ROC (MLSys 2020)</strong>是针对图神经网络（GNN）提出的分布式多GPU训练和推理框架。由于分布式训练，图神经网络中往往存在大量的数据传输，通过对图中每个顶点的设备做一定的规划，可以减少总的数据传输开销。（动态规划算法）</p>
  </li>
  <li>
    <p><strong>针对强化学习的框架</strong></p>

    <p><strong>Ray（OSDI 2018）</strong>是针对强化学习提出的一个分布式训练、模拟、推理框架。因为强化学习任务往往需要由某些设备与实际环境发生交互，因此存在端云结合的分布式场景。并且，强化学习任务往往包含大量的细粒度计算，以及异构的计算，运算时间以及资源的利用情况差别很大，传统的机器学习框架往往是为大量的、重复的计算任务设计的（例如Tensorflow，主要是为监督学习设计的，其计算任务往往包含大量的、迭代的计算）并不能很好地适用于这类情况。</p>
  </li>
  <li>
    <p><strong>联邦学习 Federated Learning</strong></p>

    <p>随着终端设备计算能力的提高以及人们对隐私和安全性的重视，用户在本地更新、训练模型成为一大趋势，但由于单个用户往往不能持有训练的全部数据，可能会由多个用户联合，在本地，分布式地训练一个模型，这就是联邦学习。联邦学习与传统的分布式训练最大的区别在于high degrees of <em>systems and statistical heterogeneity</em>（每个用户持有的数据以及设备的性能存在很大差别）。<strong>Federated Optimization in Heterogeneous Networks (MLSys 2020)</strong> 通过在现有的联邦学习框架中增加对设备异构性的支持，解决了联邦学习过程中，算法收敛性得不到保证的问题。</p>
  </li>
</ul>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="Deep Learning" /><summary type="html"><![CDATA[这一周主要是看论文，浏览了今年MLSys，ASPLOS以及去年SOSP和前年OSDI上与系统和机器学习相关的文章，重点阅读了一些与深度学习的性能提升相关的文章。受到之前包老师推荐阅读的文章（There’s plenty of room at the Top）的影响，我把这些文章分为三类，分别是Algorithm、Software和Hardware Architecture。由于我关注的这些会议都是偏系统的，所以Software层面的文章比较多。]]></summary></entry><entry><title type="html">GPU Performance: Black-box and White-box Perspectives</title><link href="https://vamix.github.io/posts/2020/08/gpu-performance/" rel="alternate" type="text/html" title="GPU Performance: Black-box and White-box Perspectives" /><published>2020-08-08T00:00:00-07:00</published><updated>2020-08-08T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2020/08/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2020/08/gpu-performance/"><![CDATA[<p>最近调研了一些GPU性能模型相关的文章。这些模型关注通用场景下GPU kernel的运算性能，往往将GPU kernel看做一个黑盒子（而事实也确实如此），通过benchmark以及profiling tools给出的一些信息，来对kernel的运行时间进行预测。同时，在另一方面，为了能深入理解程序员对CUDA kernel的优化过程，我也阅读了NVIDIA提供的CUDA Best  Practices Guide[1]。直接从程序员编写CUDA代码的角度来理解CUDA kernel可能存在的一些特性。现在将这两部分内容做一些整理和总结，从两个角度来对GPU kernel的性能进行分析。
 <!--more--></p>

<h2 id="black-box-general-gpu-performance-model">Black-box: General GPU Performance Model</h2>

<h3 id="roofline-model"><strong>Roofline Model</strong></h3>

<p>Roofline 模型[2]其实并不是针对GPU提出的性能模型，而是可以被应用到各类处理器的一个通用的性能模型。</p>

<p>Roofline 模型使用了<strong>Operation Intensity</strong>这个指标，它表示operations per byte of DRAM traffic，简单地说，与Arithmetic Intensity类似，即每Byte执行的操作数。而Operation Intensity更加通用，不仅可以表示算术指令，也能表示非算术指令。<strong>值得注意的是，Operation Intensity所使用的DRAM traffic，是指Cache和Memory之间的数据量，而非处理器和Cache 之间的数据量。</strong></p>

<p>有了Operation Intensity (Op/Byte)，再乘上Memory Performance (Bytes/s)，就能得到Floating-point Performance (Op/s, Flops/s)。Roofline模型就是将这三个关键指标统一到一张二维图中。如下图所示。</p>

<p><img src="/images/GPF/GPF-001.png" width="270" height="高度" alt="图片名称" align="center" /></p>

<p>Roofline模型通过如下公式得到：</p>

<p><strong>Attainable GFlops/sec = Min(Peak Floating Point Performance, Peak Memory Bandwidth x Operational Intensity)</strong></p>

<p>其中，峰值浮点性能以及峰值访存性能可以通过硬件厂商给出的参数信息得到，但实际性能往往会比较低，因此可以通过一些工具（例如Berkeley开源的<a href="https://crd.lbl.gov/departments/computer-science/par/research/roofline/software/ert/">Empirical Roofline Tool</a>）来测试当前硬件的实际Roofline模型。</p>

<p>Roofline模型给出的是程序可以达到的峰值性能。例如，图中左侧的红色虚线表示，在当前机器上，算术密度为1/2的程序，其能达到的峰值性能是8GFLOPs/s，由于没有达到当前机器的峰值浮点性能，所以这个程序是Memory-bound。但这不代表所有算术密度为1/2的程序都能达到8GFLOPs/s的浮点性能，程序很可能落在这条粉色虚线的任意一个位置，这跟程序自身的设计有关。当程序的算数密度较大时，可能落在右侧粉色虚线上，这时，程序就是Compute-bound。</p>

<p>程序之所以不能达到峰值性能，有很多原因。例如，memory coalescing（即把多次访存合并）在真实的程序上并不能很好地实现。例如，在GPU 的shared memory中，如果同一个warp的多个线程访问相同的bank，就会存在bank confllic，从而降低了访存的吞吐量。这些因素与程序具体的数据大小、数据分布方式、访存的步长、访存地址等有关，无法简单地用一个固定的指标来表示。</p>

<p>因此，Roofline模型其实对程序内部的实现一无所知，也不能用来对程序的性能做预测，只能用来粗略地对程序进行一个二分类：Memory-bound or Compute-bound？然后根据这个分类结果有针对性地去对程序进行优化。</p>

<p>例如，我们可以使用SIMD指令、Balance Floating-point Operation Mix来改善Compute-bound，增加浮点运算性能。改变访存的stride特性、Ensure Memory Affinity、使用软件预取等方式来改善Memory-bound，增加访存性能。当我们对程序进行了一些优化后，Roofline模型也会相应地发生改变，这篇文章称之为在Roofline模型中增加了“Ceiling”，来可视化这些优化效果，如下图所示：</p>

<p><img src="/images/GPF/GPF-002.png" width="400" height="高度" alt="图片名称" align="center" /></p>

<p>以上就是Roofline模型的介绍以及使用Roofline模型来指导程序优化的过程。对于同一台机器，其Roofline模型是固定不变的，只要我们通过一定的方式得到其Roofline模型，那么就能用这一个模型来指导不同问题的性能优化。</p>

<h3 id="cache-aware-roofline-model-for-gpu"><strong>Cache-aware Roofline Model for GPU</strong></h3>

<p>上文提到的Roofline模型是针对各类处理器的通用模型，文章[3]则将Roofline模型应用于GPU，并且对GPU内部复杂的层级化存储进行了分析，通过Cache-aware Roofline Model来对GPU kernel的性能进行分析。</p>

<p><img src="/images/GPF/GPF-008.png" width="500" height="高度" alt="图片名称" align="center" /></p>

<h3 id="quadrant-split-model"><strong>Quadrant-split Model</strong></h3>

<p>Roofline模型是device-centric，用于对同一个设备上不同问题进行分析，Quadrant-split 模型[4] 则是application-centric，用于对同一个问题运行于不同的设备上进行分析。</p>

<p>Quadrant-split 模型的横坐标是访存带宽，纵坐标是浮点性能，而直线斜率代表了问题的算术密度（即Roofline模型中的Operation Intensity）。如下图所示，每一个硬件设备都能在图中找到一个对应的位置。例如图中的TITAN X，虽然有比较大的访存带宽，但浮点性能有限，最终该程序的峰值浮点性能即为TITAN X的峰值浮点性能，表明该程序在TITAN X上是Compute-bound。</p>

<p><img src="/images/GPF/GPF-003.png" width="500" height="高度" alt="图片名称" align="center" /></p>

<h3 id="基于roofline的时间预测模型"><strong>基于Roofline的时间预测模型</strong></h3>

<p>文章[4]基于Roofline模型的思想，提出了一套工具链来预测某个GPU kernel在target GPU上的运行时间。整个过程如下图所示：</p>

<p><img src="/images/GPF/GPF-004.png" width="350" height="高度" alt="图片名称" align="center" /></p>

<ol>
  <li>
    <p>收集GPU kernel的一些指标，例如浮点运算次数、指令数、访存次数等。这些指标是与硬件无关的，所以在一个reference GPU上收集，也能被用于后续预测其他target GPU上的性能。</p>
  </li>
  <li>
    <p>利用第1步收集的硬件指标，计算kernel parameters，如下表所示：</p>

    <p><img src="/images/GPF/GPF-005.png" width="450" height="高度" alt="图片名称" align="center" /></p>
  </li>
  <li>
    <p>在target GPU上执行micro-benchmark，收集GPU的峰值性能（浮点、访存）。</p>

    <p><img src="/images/GPF/GPF-006.png" width="450" height="高度" alt="图片名称" align="center" /></p>
  </li>
  <li>
    <p>用以上两张表格中的各类指标来建模GPU kernel的运行时间。</p>

    <p>这篇文章建模kernel运行时间的主要思路是，考虑这个kernel里的各类指令，针对不同指令所占的比例、指令的operation intensity等因素，给出kernel的准确的Compute throughput的预测。（注意，预测的是compute throughput，即Roofline模型的纵轴。）</p>

    <p>首先根据dominant Instruction type给出初始Compute throughput：$T_{op}$。然后定义一个指令权重$W_{op}=\frac{T_{SP}}{T_{op}}$。以及指令的密度$D_{op} = \frac{I_{op}}{T_{total}}\times 100\%$。结合指令密度和权重，得到指令的相对开销$C_{op} = D_{op}\times W_{op}$。类似的，我们可以得到访存指令以及其他指令的相对开销：$C_{ldst}$, $C_{other}$。</p>

    <p>根据以上得到的这些中间参数，可以定义一个指令efficiency：$E_{inst}=\frac{C_{op}}{C_{op}+ C_{ldst}+C_{other}}\times 100\%$</p>

    <p>另外，并不是所有同类型的指令都执行相同数量的操作。GPU中常用的一类指令是Multiply–Add instruction，一条指令执行两次操作，一次乘法，一次加法。如果一个程序中全部是Multiply–Add instruction，那么它的compute throughput就会很高。我们用Operation mix efficiency来表示程序与峰值性能的相对比例：$E_{mix}=\frac{M_{fp32}+M_{fma32}}{2\times M_{fp32}}\times 100\%$.</p>

    <p>结合以上得到的两个Efficiency，我们可以对原始的Compute throughput作以下调整：</p>

    <p>$T'_{op}=E_{mix}\times E_{inst} \times T_{op}$</p>

    <p>根据对kernel做profiling得到的数据可以求出kernel的Operation Intensity：$O_{krn}=W_{comp}/W_{trans}$，通过我们的建模调整后的device Operation Intensity为:</p>

    <p>$O_{dev} = T'_{op}/B_{mem}$</p>

    <p>那么最终给出的Compute throughput预测值为：</p>

    <p>$T'_{op} (Compute-bound)$</p>

    <p>$O_{krn}\times B_{mem} (Memory-bound)$</p>
  </li>
</ol>

<p><strong>分析：</strong>完全基于metric来对GPU kernel的时间做预测，感觉意义不是很大。既然能够profiling这么多的硬件指标，为何不直接profiling目标程序的性能？（这也是我们的工作被质问的一点）。而且，在在这一整套建模的流程中，需要估计的量太多，每个估计量都存在一定的误差，最后将这些误差叠加，会导致误差被放大。我认为这也是这个工作的预测准确率不是很高的原因（在30个GPU kernel，6种GPU上的平均绝对误差27.66%，最差的情况下有超过100%的误差。如果使用均方根误差率，效果应该更差。）</p>

<h3 id="基于abstract-instruction-queue-model的时间预测模型"><strong>基于Abstract Instruction Queue Model的时间预测模型</strong></h3>

<p>文章[5] 提出了一个Abstract Instruction Queue Model，将GPU kernel内部的指令执行过程抽象为4个步骤：从Global Memory中读数据、从Shared Memory中读数据、计算、向Global Memory中写数据。根据一个kernel中不同类型指令的比例，可以将GPU kernel分为两大类：Compute-bound和Memory-bound。</p>

<p><img src="/images/GPF/GPF-007.png" width="650" height="高度" alt="图片名称" align="center" /></p>

<p>文章是提前通过GPU给出的指标，确定每类指令所需的GPU cycle数，然后建模一个GPU kernel所需的总cycle数，再乘以GPU一个cycle所需的时间（频率的倒数），得到一个kernel的运行时间。</p>

<ul>
  <li>
    <p><strong>Memory-bound 时间建模</strong></p>

    <p>由于计算指令完全被访存指令Overlap，因此只需要建模访存指令的时间开销。</p>

    <p>迭代次数： $N_{iter} = \lceil \frac{block_{num}}{sm_{num}}\rceil$ （这里的block是kernel任务划分后的thread block。所有的block不能一次执行完，因为硬件流处理器有限，只能多次迭代执行。而GPU的并行特性使得总时间与block数量无关，而跟迭代次数有关。从这个公式能体现出GPU的“跳变现象”，但此处的是Wave Quantization，我们论文中关注的是Tile Quantization，具体区别请见Performance Model of Convolution on GPU）</p>

    <p>一次block iteration中的Global Load指令数：$I=\frac{GL_{dispatch}}{N_{iter}}$ （$GL_{dispatch}$：Kernel总的Global Load数）
一次block iteration中的Shared Load指令数：$M=\frac{S_{dispatch}}{N_{iter}}$ （$S_{dispatch}$：Kernel总的Shared Load数）
一次block iteration中的Global Store指令数：$L=\frac{GS_{dispatch}}{N_{iter}}$ （$GS_{dispatch}$：Kernel总的Global Store数）
一个block（或者说一次block iteration）总的clock cycle数：$C_b = I + M + L$
一个GPU kernel总的clock cycle数：$C_k=C_b\times N_{iter} + L_{GS}$ ($L_{GS}$：Global Store Latency)</p>
  </li>
  <li>
    <p><strong>Compute-bound时间建模</strong></p>

    <p>对于Compute-bound类型的kernel，一个最大的难点就是如何确定计算指令和访存指令Overlap的程度。在这篇文章中，作者提出用GPU中的计算单元和访存单元的数量来表示overlap的程度：
$m = \frac{SP_{num}}{LDST{num}}$
其他指标的建模与Memory-bound类似。</p>

    <p>一次block iteration中的计算指令数：$K=\frac{CP_{dispatch}}{N_{iter}}$（$CP_{dispatch}$：Kernel总的计算指令数）
一个block（或者说一次block iteration）总的clock cycle数：$C_b = I + 1 + L_S +\lceil \frac{K}{m} \rceil + L$
一个GPU kernel总的clock cycle数：$C_k=C_b\times N_{iter} + L_{GS}$ ($L_{GS}$：Global Store Latency)</p>
  </li>
</ul>

<p>分析：这篇文章提出的Abstract Instruction Queue Model将GPU内部的执行过程可视化了，比metric-driven的模型要直观许多，但是这样的高度抽象过于简略。对于Compute-bound类型的kernel，如何确定overlap的程度不能直接用SM和Load/Store unit的比例来近似，真实情况会复杂很多。并且，这篇文章的方法需要我们对kernel本身有一定程度的了解，例如，知道它是Memory-bound还是Compute-bound，知道这个kernel会被划分为多少个block，这些信息不一定能得到（例如cuDNN）。</p>

<h2 id="white-box-cuda-optimization">White-box: CUDA Optimization</h2>

<ul>
  <li>
    <p><strong>访存对于GPU kernel的性能影响最大。</strong> 不论是从Black-box视角去建模程序的性能，还是从White-box的视角去优化程序的性能，访存往往都是瓶颈。而GPU内部的层次化存储结构以及Cache的使用，使得准确建模访存开销十分困难。</p>
  </li>
  <li>
    <p><strong>尽量减少Host Memory与Device Memory之间的拷贝。</strong>GPU与Device Memory的带宽（898 GB/s on the NVIDIA Tesla V100, for example）远大于Host Memory和Device Memory之间的带宽（16 GB/s on the PCIe x16 Gen3）。并且每次传输会引入额外的开销，因此最好将多次小的传输batch成一次大的传输，即使这会引入因为访存区域不连续带来的packing和un-packing开销。</p>
  </li>
  <li>
    <p><strong>Coalesced Access to Global Memory</strong></p>

    <p>一个warp中所有线程并发访问Global Memory时，这些请求会被合并成一些transactions，每个transaction访问Global Memory中的32 bytes数据。例如，一个warp中的32个线程，如果每个线程访问global memory中相邻的一个4-byte 数据(float)，那么这32个线程的request会被合并成4个transactions，在图中用红色区域表示：</p>

    <p><img src="/images/GPF/GPF-009.png" width="400" height="高度" alt="图片名称" align="center" /></p>

    <p>如果这32个线程的数据并不是刚好对齐的，那么将会产生5次transactions：</p>

    <p><img src="/images/GPF/GPF-010.png" width="400" height="高度" alt="图片名称" align="center" /></p>

    <p>图中展示了一个简单的copy kernel，当数据不对齐时，达到的峰值带宽只有对齐时的90%。</p>

    <p><img src="/images/GPF/GPF-011.png" width="450" height="高度" alt="图片名称" align="center" /></p>
  </li>
  <li>
    <p><strong>Strided Access to Global Memory</strong></p>

    <p>当一个warp中的每个线程按照一定的步长访问global memory，也会造成有效带宽的降低，因为每次transaction只能访问连续的数据，那么步长越大，浪费的带宽就越多，有效带宽就越低。如图所示：</p>

    <p><img src="/images/GPF/GPF-012.png" width="450" height="高度" alt="图片名称" align="center" /></p>

    <p>因此，应该尽量避免这样的访存模式。或者使用shared memory。</p>
  </li>
  <li>
    <p><strong>Shared Memory</strong></p>

    <p>Shared Memory比Global Memory有着更高的带宽，更低的延迟和更小的容量。Shared Memory被划分为32-bit大小的bank，即每个bank只能存一个float数，每个bank每个cycle的带宽就是32bit。同一个warp中的32个线程如果访问不同的数据，则可以在同一个clock cycle被并行处理。如果存在多个线程访问同一个bank，则会被硬件线性化访问，从而使得有效带宽降低。</p>

    <p>Shared Memory常用于同一个block中的线程需要重复使用Global Memory中的数据的情况。这时，被重用的数据可以提前取到Shared Memory中，避免多次从Global Memory中取数带来的巨大时间开销。例如分块矩阵的乘法C=AB，C的每个分块的结果由多个线程对矩阵A和B的分块计算得到，而A和B的数据会被重复利用，则可以从Global Memory提前取到Shared Memory中。</p>
  </li>
  <li>
    <p><strong>Cache Hierarchy</strong></p>

    <p><img src="/images/GPF/GPF-013.png" width="500" height="高度" alt="图片名称" align="center" /></p>

    <p>在NVIDIA GPU中，Cache的层级化结构如图所示(来自 <a href="https://docs.nvidia.com/gameworks/content/developertools/desktop/analysis/report/cudaexperiments/kernellevel/memorystatisticscaches.htm">NVIDIA</a> )，分为Data Cache（L1、L2）和Texture Cache。L1的一次transaction是128bytes，L2和Texture Cache都是32bytes。因此，一次L1 Cache Miss将会引起4次L2 Cache Access。</p>

    <p>何时访问什么Cache：</p>

    <p>Global Memory其实是一个逻辑概念，实际上对应于GPU中的Device Memory（DRAM）。一次Global Memory Access并不等于一次DRAM Access，因为要访问的数据可能存在于Cache中，如果在Cache中命中，则不再访问DRAM。Global Memory Access可以分为Global Read Only和Global两类数据通路。Global Access可以使用L1/L2 Cache。而Global Read Only可以额外使用Texture Cache。</p>

    <p>Texture Cache采用了与Data Cache非常不同的合并规则（coalescing rules），当需要访问规律性不强的内存地址时，使用Texture Cache可能带来更好的性能。Texture Memory是Device Memory中的一块只读区域。Texture Memory（以及Texture Cache）是专门为访问2维数组而进行了空间局部性优化的一种内存。因此，如果Texture Cache Hit Rate不高，代表程序访存的模式与2维数组的访存模式不太匹配。</p>
  </li>
  <li>
    <p>GPU的Memory Hierarchy以及Data Path如下图所示(来自<a href="https://stackoverflow.com/questions/37732735/nvprof-option-for-bandwidth/37740119#37740119">StackOverflow</a>)：</p>

    <p><img src="/images/GPF/GPF-014.png" width="550" height="高度" alt="图片名称" align="center" /></p>

    <p>这张图标注出了GPU整个存储架构中可能存在的数据通路。跟nvprof给出的metrics存在如下对应关系：</p>

    <ol>
      <li><code class="language-plaintext highlighter-rouge">dram_read_throughput</code> <code class="language-plaintext highlighter-rouge">dram_read_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">dram_write_throughput</code> <code class="language-plaintext highlighter-rouge">dram_write_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">system_read_throughput</code> <code class="language-plaintext highlighter-rouge">system_read_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">system_write_throughput</code> <code class="language-plaintext highlighter-rouge">system_write_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">l2_l1_read_transactions</code>  <code class="language-plaintext highlighter-rouge">l2_l1_read_throughput</code></li>
      <li><code class="language-plaintext highlighter-rouge">l2_l1_write_transactions</code>  <code class="language-plaintext highlighter-rouge">l2_l1_write_throughput</code></li>
      <li><code class="language-plaintext highlighter-rouge">l2_tex_read_transactions</code> <code class="language-plaintext highlighter-rouge">l2_texture_read_throughput</code></li>
      <li>Texture是只读的，这个数据通路不存在</li>
      <li><code class="language-plaintext highlighter-rouge">shared_load_throughput</code> <code class="language-plaintext highlighter-rouge">shared_load_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">shared_store_throughput</code> <code class="language-plaintext highlighter-rouge">shared_store_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">l1_cache_local_hit_rate</code></li>
      <li>l1 是 write-through cache，这个通路上不存在单独的指标</li>
      <li><code class="language-plaintext highlighter-rouge">l1_cache_global_hit_rate</code></li>
      <li>same as 12</li>
      <li><code class="language-plaintext highlighter-rouge">gld_efficiency</code> <code class="language-plaintext highlighter-rouge">gld_throughput</code> <code class="language-plaintext highlighter-rouge">gld_transactions</code></li>
      <li><code class="language-plaintext highlighter-rouge">gst_efficiency</code> <code class="language-plaintext highlighter-rouge">gst_throughput</code> <code class="language-plaintext highlighter-rouge">gst_transactions</code></li>
    </ol>
  </li>
</ul>

<p><strong>一些思考：</strong></p>

<p>从黑盒视角来看，GPU的性能可以被一些硬件指标和程序本身的指标完全建模，只要两个程序具有相同的浮点运算次数、指令类型、指令效率，硬件存在相同的浮点运算性能和访存带宽，那么这两个程序的运行时间应该是一样的，然而从白盒视角来看，程序内部一个warp中的两个线程，甚至两个warp的线程之间，访问的内存地址不同，就可能造成不同次数的shared memory conflict以及不同的cache miss rate。因此一个准确的性能模型必须对程序本身所执行的操作有一定的了解，才有可能将这些复杂的访存行为纳入其中。因此，我们在构建神经网络中单个op的性能模型时，要避免使用构建通用的GPU kernel性能模型的思路，纯metrics-drien，而要加入我们已有的机器学习背景知识。</p>

<p>另外，在建模时，模型的复杂度也是一个值得深入思考的问题。Roofline模型以及很多Roofline的变体，都是为了用于指导程序性能优化，而非预测程序的运行时间，因此其往往过于简单。而另一些模型，例如文章[4] [5]，通过收集大量的硬件指标以及构建抽象的instruction queue来对程序的性能做建模，往往是为了模型更加通用，所以显得过于复杂。当我们只需要构建一个或多个针对神经网络op的性能模型时，其实问题的scope是很小的，并不要求模型有很强的通用性，只需要关注准确性，那么是否可以利用我们对op的一些知识舍弃模型的通用性而提高准确性呢？也是需要思考的问题。</p>

<h2 id="参考文献">参考文献</h2>

<p>[1] <a href="https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#abstract">CUDA C++ Best Practices Guide</a><br />
[2] Williams, Samuel, Andrew Waterman, and David Patterson. “<strong>Roofline: an insightful visual performance model for multicore architectures.</strong>” <em>Communications of the ACM</em> 52.4 (2009): 65-76.<br />
[3] Lopes, André, et al. “<strong>Exploring GPU performance, power and energy-efficiency bounds with Cache-aware Roofline Modeling.</strong>” <em>2017 IEEE International Symposium on Performance Analysis of Systems and Software (ISPASS)</em>. IEEE, 2017.<br />
[4] Konstantinidis, Elias, and Yiannis Cotronis. “<strong>A quantitative roofline model for GPU kernel performance estimation using micro-benchmarks and hardware metric profiling.</strong>” <em>Journal of Parallel and Distributed Computing</em> 107 (2017): 37-56.<br />
[5] Pei, Ziqian, et al. “<strong>Iteration time prediction for CNN in multi-GPU platform: modeling and analysis</strong>.” <em>IEEE Access</em> 7 (2019): 64788-64797.</p>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="GPU" /><category term="Performance Model" /><category term="Machine Learning" /><summary type="html"><![CDATA[最近调研了一些GPU性能模型相关的文章。这些模型关注通用场景下GPU kernel的运算性能，往往将GPU kernel看做一个黑盒子（而事实也确实如此），通过benchmark以及profiling tools给出的一些信息，来对kernel的运行时间进行预测。同时，在另一方面，为了能深入理解程序员对CUDA kernel的优化过程，我也阅读了NVIDIA提供的CUDA Best Practices Guide[1]。直接从程序员编写CUDA代码的角度来理解CUDA kernel可能存在的一些特性。现在将这两部分内容做一些整理和总结，从两个角度来对GPU kernel的性能进行分析。]]></summary></entry><entry><title type="html">Performance Model of Convolution on GPU</title><link href="https://vamix.github.io/posts/2020/08/convolution-performance-model/" rel="alternate" type="text/html" title="Performance Model of Convolution on GPU" /><published>2020-06-01T00:00:00-07:00</published><updated>2020-06-01T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2020/08/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2020/08/convolution-performance-model/"><![CDATA[<h2 id="背景">背景</h2>

<h3 id="gpu-introduction">GPU Introduction</h3>

<ul>
  <li>
    <p><strong>GPU架构</strong></p>

    <p>NVIDIA GPU包含多个Stream Multiprocessor（SM）。每个SM的结构如上图所示，其中包含多个Stream Processor（SP）以及数个Special Function Units (SFU)，SP是GPU的基本算术指令执行单元，SFU则用于执行一些复杂的操作，例如sin，cos等。除此之外，SM还包含一个几十KB大小的Shared-Memory、一个Instruction Cache和Data Cache，以及一个Multithreading issuing unit用于指令的调度。</p>
  </li>
</ul>

<p><!--more--></p>

<p>​	<img src="/images/PMCG/PMCG-002.png" width="270" height="高度" alt="图片名称" align="center" /></p>

<ul>
  <li>
    <p><strong>GPU 线程</strong></p>

    <p>在设计并行程序时，一个很重要的步骤就是对任务进行分解，将任务分解为多个子任务后，使子任务并行执行从而实现加速。</p>

    <p>为了简化并行程序的编程难度，在NVIDIA GPU中，线程被组织为线程块（Thread Block），线程块又进一步被组织为线程网格（Thread Grid），如下图所示。在这样的层级化组织架构中，Grid中的每个Block都有一个Block ID，Block中的每个Thread都有一个Thread ID，通过Block ID和Thread ID组合，每一个线程就能获取到自己在整个Grid中的相对位置，也就更容易对自己需要负责的子任务进行计算，对相应的数据进行读写。</p>

    <p><img src="/images/PMCG/PMCG-003.png" width="250" height="高度" alt="图片名称" align="center" /></p>

    <p>我们使用GPU进行计算，是通过编写核函数来实现的，每个核函数可以占用一个线程网格，核函数本身就是对这个线程网格中一个线程需要执行的操作的定义。在编写核函数时，通过Block ID和Thread ID就能实现对任务的分解。在launch核函数时通过指定Grid size和block size就能限定线程网格的大小和线程块的大小。一个线程网格内的所有线程执行的都是一样的核函数，这属于SPMD。</p>

    <p>在实际执行时，线程并不是单个被调度，而是按照warp的粒度被调度。一个<strong>warp</strong>是一个block中Thread ID连续的32个线程，这是GPU中一条指令被执行的最小粒度。也就是说，一个warp是一条SIMD指令，其中的32个线程永远执行一样的指令。</p>

    <p>GPU执行一个核函数的大致流程如下：通过CUDA API launch一组线程后，GPU硬件通过一定的调度机制将线程按照block的粒度调度给SM（每个block可能被调度到任何一个SM上），SM中的调度器将block划分为warp，并按照warp的粒度将线程调度到SP上执行。当一个warp下一条指令所需的数据已经准备好了（从global memory 拷贝到了shared memory），那么这个warp就会被调度执行。</p>

    <p>Warp size是一个可能会变化的数量，但目前所有NVIDIA GPU的warp size都是32。</p>
  </li>
  <li>
    <p><strong>GPU 存储架构</strong></p>

    <p><img src="/images/PMCG/PMCG-004.png" width="350" height="高度" alt="图片名称" align="center" /></p>

    <p>GPU的存储架构如上图所示，包含以下几种内存单元：</p>

    <ul>
      <li><strong>Register</strong>。在每个SM中包含一定数量的register。register只能被单个线程所使用，一旦一个register被分配给某个线程，其他线程便无法访问这个register。register的访问延迟很低，只有1个cycle。</li>
      <li><strong>Shared Memory</strong>。Shared memory也是每个SM内部所包含的，但shared memory可供一个block内部所有的线程共享，因此可以使用shared memory存储需要在多个线程之间读取或修改的数据。shared memory一般被分为固定大小的bank。其访问延迟也只有1个cycle，但bank conflic可能会降低访问的性能。</li>
      <li><strong>Global Memory</strong>。Global memory是GPU的device memory，可供一个grid中的所有block共享，并且也是跟CPU memory进行交换的”窗口“，通过cudaMemcpy()从Host device拷贝数据到GPU，就是存放在global memory。但global memory的访问延迟非常高，需要约500个cycle。</li>
      <li><strong>Local Memory</strong>。Local memory是可供单个线程使用的一种存储空间，其本质上是global memory的一部分，当线程耗尽SM中的register时，就会使用local memory。其访问延迟跟global memory一样。</li>
      <li><strong>Texture Memory</strong>。Texture memory是可供一个grid中所有block共享的一类global memory，为图像处理进行了优化，需要通过特殊的API使用，并不常用。</li>
    </ul>
  </li>
  <li>
    <p><strong>跳变现象（Quantization）</strong></p>

    <p><img src="/images/PMCG/PMCG-005.png" width="400" height="高度" alt="图片名称" align="center" /></p>

    <p>介绍完GPU的架构和线程执行流程后，我们就能比较容易理解GPU中的”跳变“现象（Quantization）。这里有一个矩阵乘的例子，假设如图的矩阵(a)要和另一个矩阵做乘法，我们通过分块矩阵乘实现并行计算。如果我们设置的分块大小为128x128，那么这个矩阵刚好能被分为4块，每一个线程执行的都是有用的计算，没有任何性能被浪费，非常完美。但是如果矩阵(b)是一个256x257的矩阵，不能刚好被划分为4块，需要被划分为6块，那么多出来的两块也会执行和其他分块一样多的计算量，耗费一样多的时间。但其中绝大部分是无用的计算，虽然程序的正确性能够被保证，但性能被极大地降低了。如果对矩阵乘（$K\times N$与$N\times M$相乘）这个例子在维度N上进行连续的测试，就能得到以下结果：图中横坐标代表$N$的大小，纵坐标代表乘法时间，从图中可以看到非常明显的”跳变“现象，$N$从256增加到257时，时间大约增加了50%。</p>

    <p><img src="/images/PMCG/PMCG-006.png" width="300" height="高度" alt="图片名称" align="center" /></p>

    <p>不只是矩阵乘会出现这样的跳变现象，任何对计算任务做划分，并行执行子任务的CUDA程序都有可能出现这样的跳变现象，不同算法对任务的划分方式不一样，出现跳变的维度就会不同，要对CUDA算法的性能进行建模，跳变现象是不可忽视的一个重要影响因素，需要根据具体的算法进行具体的分析，才能得到比较准确的模型。</p>

    <p>以上跳变现象在NVIDIA的官方文档中被称为”Tile Quantization“，还有一类跳变被称为”Wave Quantization“，Wave Quantization跟block的数量有关，如果一个计算任务的所有block（假设为$n$个）刚好能被所有的SM（假设为$n$个）并行计算，就能得到最佳的性能，此时如果再增加一个block，为了计算这一个block，GPU所需要的时间其实跟计算$n$个block的时间是一样的，因此就会形成跳变现象。</p>
  </li>
</ul>

<h3 id="cudnn">cuDNN</h3>

<p>cuDNN是NVIDIA提供的用于加速GPU上神经网络计算过程的库，其中，卷积被实现为3大类算法：GEMM类、FFT类以及Winograd类。GEMM有三种变体：GEMM、GEMM_IMPLICIT、GEMM_IMPLICIT_PRECOMP；FFT有两种变体：FFT、FFT_TILING；Winograd有两种变体：WINOGRAD、WINOGRAD_NON_FUSED。在下面的章节中，我们将详细介绍这几类算法的实现原理及模型。</p>

<h2 id="performance-model">Performance Model</h2>

<ul>
  <li>
    <p><strong>GEMM及其变体</strong></p>

    <ul>
      <li>
        <p><strong>实现原理</strong></p>

        <p>最常见的卷积算法的实现是基于BLAS GEMM矩阵乘法的。在进行卷积之前首先将要进行卷积的图片和卷积核转化为矩阵。如图所示，是一个 3x3x3 的图片 $D$ 与两个 2x2x3 的卷积核 $F,G$ 进行卷积操作的过程。先将原始图片 $D$ 由一个三维张量转化为一个二维矩阵 $D_m$，矩阵的每一列是卷积核每一步在图片上的作用域所包含的数值所形成的一维列向量。 矩阵的列数就是卷积核总共对该图片的作用次数。再将所有的卷积核转化为一个二维矩阵 $F_m$，矩阵的每一行是一个卷积核展开形成的一维行向量，矩阵的行数就是卷积核的个数。最终两个矩阵相乘得到的结果矩阵 $O_m$ 的每一行是卷积操作输出图片的一个通道的一维展开向量，行数就是结果的输出通道数。</p>

        <p><img src="/images/PMCG/PMCG-007.png" width="450" height="高度" alt="图片名称" align="center" /></p>

        <p>在cuDNN中，GEMM不仅有直接实现的版本，也有其他两种变体。直接实现的GEMM，由于要存储转换后的中间矩阵，需要占用大量的内存。另外两种变体是IMPLICIT_GEMM和IMPLICT_PRECOMP_GEMM，这两种实现都不需要存储中间矩阵，因为他们的中间矩阵是在GEMM核函数中即时计算的，并且IMPLICT_PRECOMP_GEMM还会提前调用另一个核函数提前计算一些indice，用于即时的矩阵转换过程。</p>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>WINOGRAD</strong></p>

    <ul>
      <li>
        <p><strong>实现原理</strong></p>

        <p>这一类卷积算法基于Winograd’s minimal filtering algorithms来实现。其本质上是通过增多加法次数来减少乘法次数，因为加法比乘法耗时更短，因此这类算法能在某些参数组合下带来很大的性能提升。由于Winograd算法中的加法次数随着输入图片的大小呈平方增大，因此Winograd算法只适用于较小的输入。</p>

        <p>在cuDNN中，Winograd算法也有两种实现，一种是fused Winograd，一种是non-fused Winograd，区别就在于对于输入、卷积核的转换过程一并放到Winograd核函数中还是单独用一个核函数来计算。</p>

        <p>现阶段的cuDNN对于Winograd算法的使用有一定的限制条件：对于Winograd（fused）只支持3x3卷积和步长为1的情况，对于Winograd no-fused，只支持3x3卷积和5x5卷积，步长为1的情况。</p>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>FFT</strong></p>

    <ul>
      <li>
        <p><strong>实现原理</strong></p>

        <p>两个输入的卷积的傅里叶变换等于这两个输入的傅里叶变换的乘积，即时域中的卷积等于频域中的按位乘法。因此，我们要进行卷积操作时，可以先对输入图片和卷积核进行傅里叶变换，再对变换后的结果进行乘法操作，将计算得到的结果进行逆傅里叶变换，从而得到输出图片。这就是用傅里叶变换实现卷积的原理。之所以傅里叶变换能在某些参数组合下带来性能的提升，是因为乘法操作比卷积操作的计算量更小，因此，如果对输入和输出进行傅里叶变换的时间相比总时间较少，那么总体性能就能得到提升。然而，对单个图片或卷积核进行傅里叶变换的开销是比较大的，因此FFT类算法更适用于输入图片数量（batch size）较大或卷积核比较多（out_chan）、比较大（kernel_wid）的情况，因为对输入进行傅里叶变换的结果会被多次使用，分摊了傅里叶变换的开销。</p>

        <p>不过目前深度学习领域的主流趋势是使用较小的卷积核，因为研究者发现，使用多个较小的卷积核就能模拟一个大卷积核的效果，且执行的计算次数大幅减少，能提高神经网络的性能。因此，FFT及其变体在实际的神经网络上很少会被用到。</p>

        <p>现阶段的cuDNN对于FFT类算法的使用有一定的限制条件：对于FFT，其输入图片加上padding之后的宽度和高度不能超过256，只支持步长为1的情况，并且要求卷积核高度大于padding高度，卷积核宽度大于padding宽度；FFT_TILING是FFT的一个变体，其实现上就是把输入图片分块为更小的图片，因此这个算法对输入图片的大小没有要求，其余要求与FFT相同。</p>
      </li>
    </ul>
  </li>
</ul>

<p><strong>TO BE UPDATED: 我们针对各类算法提出了相应的性能模型，目前正在进一步优化。模型完善后将会更新本文档。</strong></p>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="GPU" /><category term="Performance Model" /><category term="Machine Learning" /><summary type="html"><![CDATA[背景 GPU Introduction GPU架构 NVIDIA GPU包含多个Stream Multiprocessor（SM）。每个SM的结构如上图所示，其中包含多个Stream Processor（SP）以及数个Special Function Units (SFU)，SP是GPU的基本算术指令执行单元，SFU则用于执行一些复杂的操作，例如sin，cos等。除此之外，SM还包含一个几十KB大小的Shared-Memory、一个Instruction Cache和Data Cache，以及一个Multithreading issuing unit用于指令的调度。]]></summary></entry><entry><title type="html">分布式系统和机器学习-文献阅读报告</title><link href="https://vamix.github.io/posts/2020/05/distributed-systems-and-ML/" rel="alternate" type="text/html" title="分布式系统和机器学习-文献阅读报告" /><published>2020-05-07T00:00:00-07:00</published><updated>2020-05-07T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2020/05/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2020/05/distributed-systems-and-ML/"><![CDATA[<h2 id="摘要">摘要</h2>

<p>我选择的20篇文章主要关注分布式系统，以及分布式系统在机器学习中的应用。分布式系统是计算机科学历史悠久的一个研究方向，设计一个分布式系统需要考虑许多方面的因素，例如进程的交互与通信协议、故障处理及容错机制、数据复制及一致性等。经过学界多年的研究，针对这些问题，已经有了比较好的解决方案。而在当下，机器学习正迅速发展，越来越多的数据以及规模越来越大的模型，都使得在分布式环境中训练机器学习模型成为一大主流趋势。如何针对机器学习应用设计分布式系统，从而加速机器学习应用的训练过程，成为现在研究的热潮。<!--more--> 2018年在斯坦福举行的全新学术会议SysML，就着眼于机器学习和系统这个交叉学科，使得这个领域受到越来越多的关注。通过这次的文献阅读，我们将会看到，适用于机器学习的分布式系统与传统的分布式系统有许多内在关联，但同时，机器学习应用的一些内在特点使其不同于传统的并行程序，如何针对机器学习应用的特点设计分布式系统，充满了新的挑战。</p>

<h2 id="abstract">Abstract</h2>

<p>The 20 articles I chose mainly focused on distributed system and its application in machine learning. Distributed system is a research field with a long history in computer science. Designing a distributed system requires consideration of many factors, such as process interaction and communication protocols, fault tolerance mechanisms, data replication, and data consistency. After years of research, there have been lots of good solutions to these problems. At the moment, machine learning is developing rapidly. More data and larger models have made the training of machine learning models in a distributed environment a mainstream. SysML, a new academic conference held at Stanford in 2018, focused on the interdisciplinary field of machine learning and systems, which has attracted more and more attention in this field. Through this literature reading, we will see that the distributed system for machine learning has many internal relations with the traditional distributed system, but at the same time, some inherent characteristics of machine learning applications make it different from traditional parallel programs. How to design a distributed system for machine learning applications is full of challenges.</p>

<p>我选择的20篇文章可以分为以下四类：</p>

<ul>
  <li>
    <p><strong>分布式系统领域的经典文章。</strong>这部分文章大多都已经入选ACM SIGOPS名人堂。SIGOPS名人堂奖是SIGOPS组织于2005年设立的奖项，每年都会评选几篇发表了10年以上，对系统领域产生了巨大影响的文章。我选择的文章包括Leslie Lamport关于分布式系统中的时间[19]以及Paxos协议[20]的两篇文章，以及MapReduce[1]、Spark[2]这样著名的工作和来自工业界三篇文章[16] [17] [18]；</p>
  </li>
  <li>
    <p><strong>著名的分布式机器学习系统。</strong>其中包含了Google的DistBelief[3]、Microsoft的Adam[4]、CMU的GraphLab[5]、Parameter Server[6]和一个CPU集群上的分布式框架[7]；</p>
  </li>
  <li>
    <p><strong>分布式机器学习算法的设计。</strong>其中包括并行的随机梯度下降算法[9]、中心化和去中心化算法的对比[13]以及Stale Synchronous Parallel算法[14]；</p>
  </li>
  <li>
    <p><strong>机器学习与系统结合的一些文章</strong>[8] [10] [11] [12] [15]。</p>
  </li>
</ul>

<h2 id="分布式系统领域的经典文章"><strong>分布式系统领域的经典文章</strong></h2>

<p>许多分布式系统领域的经典文章，其包含的思想可以对我们设计具体的分布式系统或算法起到指导作用。 本章节的内容包括MapReduce和Spark两大分布式编程框架，Google的Chubby锁服务，Microsoft的DryadLINQ分布式并行计算系统，Amazon的Dynamo分布式存储系统，以及Leslie Lamport对分布式系统时钟的经典论述和对Paxos协议的详细刻画。</p>

<h3 id="mapreduce">MapReduce</h3>

<p>MapReduce[1]是Google提出的针对大规模数据进行计算的编程框架。MapReduce对程序模型的抽象非常简单，将程序分为Map和Reduce两个阶段，Map要做的工作就是把数据转换成key-value的形式，而Reduce则会对Map输出的key-value进行聚合操作。每一个Map 和Reduce操作都可以运行在分布式集群中的任何一个节点上，而用户不需要考虑具体的任务分配细节。MapReduce通过隐藏编程框架背后复杂的并行化机制、容错机制、数据局部性优化和负载均衡等实现细节，仅仅向程序员提供一个简单明了的编程接口，使得编写大型分布式程序更加容易。程序员需要关心的只有Map和Reduce这两个函数功能的实现。</p>

<p>在MapReduce之前也有一些并行程序框架，但这些框架都只是在小规模集群上进行了实现，没有扩展到大规模集群，同时，MapReduce提供了一个带有容错功能的实现，而其他框架没有解决容错的问题。River是一个提供负载均衡的编程框架，其负载均衡是通过调度磁盘和网络带宽的占用，而MapReduce通过限制编程模型，从而可以更加容易地把任务划分为更细粒度的子任务，实现负载均衡。</p>

<p>MapReduce对“掉队者”的处理如下：一旦一个Worker完成了它所有的计算任务，且没有新任务可以调度，Master就会把仍然还在别的Worker上计算的任务再次调度给空闲节点，这些任务叫做“备份任务”，一旦备份任务先完成，就把这个任务标记为完成，不再等待“掉队”的节点，避免掉队者拖慢整体任务的完成时间。</p>

<h3 id="spark">Spark</h3>

<p>Spark[2]的提出，是为了解决MapReduce访存开销大的问题。因为MapReduce没有对分布式内存进行抽象，每次Map和Reduce计算完成后，会把结果写回分布式存储中，以供下次再使用，对于一些数据重用率比较高的程序，例如机器学习和图算法，包括PageRank, K-means 聚类和逻辑斯蒂回归等，以及交互式的数据挖掘任务，就会产生大量的网络传输和磁盘读写开销。</p>

<p>Spark的作者因此提出了Resilient Distributed Datasets（RDD），一种分布式内存抽象。RDD可以帮助程序员在大规模集群上实现in-memory的计算，通过把数据保存在内存中来提高运算效率，并保证容错性。Spark则是对RDD的一个具体实现。</p>

<p>RDD实现更高性能的核心是通过对数据的操作模式进行更多的限制来得到更高的效率，比如RDD只适用于粗粒度、批处理的数据，不能实现对任意位置数据的任意读写，但正因批处理，使得RDD在容错处理、故障恢复时更加高效。同时，RDD的数据是不可修改的，这使得处理掉队者的问题时也很简单，类似MapReduce，直接发起“备份任务”重新对掉队者的任务进行计算即可。</p>

<h3 id="chubby">Chubby</h3>

<p>接下来我阅读了几篇来自工业界的研究成果，其中包括Google的Chubby锁服务、Amazon的Dynamo分布式存储系统以及Microsoft的DryadLINQ。正如Google在介绍Chubby的文章中所说，这些工业界的学术成果，大多不含有令人耳目一新的设计，其使用的技巧基本都是学术界已有的成果，这些工作更偏重于工程实现，而非学术研究。但是阅读工业界的论文让我看到了如何解决实际部署系统时所遇到的在设计阶段没有设想到的问题，这是十分有意义的。</p>

<p>Chubby[16]是Google提出的分布式锁服务，其本质上是一个分布式的文件系统。服务调用者可以通过读写文件的方式实现加锁和释放锁。并且在Google的GFS中，Chubby还被用来选择master server，以及用于存储少量的元数据。Chubby的系统架构包括一个Chubby cell以及客户端的Chubby library。Chubby cell是一个分布式的文件系统，由多个server组成，客户端的Chubby library通过RPC与Chubby cell进行通信。在Chubby cell内部的多个server之间，通过一致性协议选择一个server作为master，只有master server会处理用户请求，其余server则用于数据备份。</p>

<p>作者使用了较多的篇幅分析之所以通过这样一个分布式锁服务来实现节点之间的一致性，而不是实现一个分布式一致性协议库的原因。因为通过这样的一个锁服务，可以更容易保持已有程序的结构以及数据传输的模式，不需要对程序本身进行过多的修改，并且在一致性协议中，往往要求节点要能存储和读取小规模的数据，Chubby本身就是文件系统，使得这一点更加容易。并且，分布式一致性算法往往通过一种选举机制来做决定，这要求系统中有比较多的可用节点，但通过锁服务，即使系统中只有一个节点，也能安全地推进程序的执行。同时，Chubby采用了粗粒度加锁，作者的考虑是，采用粗粒度的锁服务，不仅能减少Chubby cell的负载，同时也能更加容易地应对server的故障。而细粒度的锁服务，一旦server出现故障，将会造成大量进程阻塞，反而降低整个系统的性能。</p>

<h3 id="dynamo">Dynamo</h3>

<p>Dynamo[18]是Amazon提出的分布式key-value存储服务。Dynamo从Amazon面临的实际问题出发，通过牺牲一定的一致性，实现了系统更高的可用性。在Dynamo之前，工业界的系统大多使用关系型数据库来存储信息，但Amazon发现在他们的应用场景中，关系型数据库复杂的query语句并不常用，大多数应用只会用到数据库的主键进行查询。因此Amazon提出了简单的key-value存储系统来满足大量的简单查询操作。</p>

<p>Dynamo通过一致性哈希算法将数据分布在不同的机器上。所有的机器组成一个哈希环，每个机器负责自己到下一个机器之间对应的哈希值。每当有数据要被存入，用其key值计算出一个哈希值，将这个数据的key-value对存入对应的机器，并将备份存入后续几个机器。除此之外，Dynamo也综合使用了大量的分布式系统中的技巧来提高系统的可用性和可扩展性：通过quorum算法以及去中心化的同步策略来保证备份之间的数据一致性，以及通过gossip算法来实现分布式的错误检测。</p>

<h3 id="dyradlinq">DyradLINQ</h3>

<p>DryadLINQ[17]是Microsoft提出的用于在大规模数据和大规模分布式集群上进行高效计算的框架。其中，Dryad是一个分布式的数据并行程序执行框架，LINQ则是对Microsoft .NET语言的扩展，赋予了.NET语言query的特性。DryadLINQ能够自动地把通用编程语言下的命令式程序编译为能在大规模集群上执行的分布式程序。这个系统的目标是，让程序员只需要编写在单个计算机上执行的程序，让系统来处理并行、调度、分布式和容错的问题。</p>

<p>DryadLINQ的执行流程如下：用户编写.NET程序，创建DryadLINQ object，然后DryadLINQ会将程序编译为分布式的Dryad 执行计划（execution plan），将原程序拆分为多个子程序并生成相应的代码，每个子程序将运行在不一样的Dryad节点，接下来DryadLINQ唤起一个Dryad任务管理器，任务管理器将初始化任务的数据流图，并将任务调度到集群的服务器上，同时任务管理器提供了容错机制，会重新执行故障节点上的任务或者掉队者的任务。任务管理器也会根据用户定义的策略来动态调整任务图。DryadLINQ最大的优势在于，通过添加对任何一种编程语言的扩展，就能轻松实现对这种编程语言的程序的分布式计算，同时LINQ强大的静态数据类型使得对程序debug非常容易。</p>

<h3 id="分布式系统中的时钟和事件顺序">分布式系统中的时钟和事件顺序</h3>

<p>Leslie Lamport是2013年图灵奖得主，他的许多工作对分布式系统领域有着非常重要的影响。这次文献阅读，我选择了他的两篇著名文章，分别对分布式系统中的时间以及Paxos协议进行了详细的阐述。</p>

<p>文章[19]显示定义了分布式系统中的偏序关系，然后引入逻辑时钟，逻辑时钟的提出是为了与现实世界的物理时钟进行区分，因为在计算机系统中，很多时候只要知道事件发生的先后关系即可，并不需要知道对应的物理时间，因此在多个节点之间进行时间同步时，只需要在事件发生的先后关系上进行同步，而不是在物理时间上进行同步。作者通过一个分布式算法来把偏序关系扩展到分布式系统中所有事件的全序关系，有了全序关系就能比较容易地解决分布式系统常常面临的复杂的同步问题。</p>

<p>然而仅仅有全序关系仍然可能出现一些“异常情况”，作者举的例子是用户A在一个互联网络发布一条消息，然后打电话给用户B，让用户B也发布一条消息，此时消息B有可能得到比消息A更小的时间戳。也就是说，全序关系并不能完全确定两个事件发生的绝对先后关系，因此作者引入了物理时钟，通过一定的限制条件，可以保证事件发生的绝对先后关系。</p>

<p>除此之外，这篇文章还给出了分布式系统的一个定义：</p>

<blockquote>
  <p><em>A system is distributed if the message transmission delay is not negligible compared to the time between events in a single process.</em></p>
</blockquote>

<p>作者从消息传递延迟的角度对分布式系统下定义，抓住了分布式系统的核心特征。</p>

<h3 id="兼职议会与paxos协议">兼职议会与Paxos协议</h3>

<p>Lamport的另一篇文章： The Part-Time Parliament[20]，分析了Paxos小岛上的兼职议会系统，从而提出了一些新的思路来设计分布式系统。这篇文章的立足点在于，Paxos小岛上的议会系统与计算机中的分布式系统有很多可以对应上的特点。例如，Paxos的议会系统由很多个立法者组成，每个立法者相当于分布式系统中的一个进程，由于Paxos小岛上每个立法者都需要从事商业活动，因此只能兼职担任立法者，当立法者离开议会大厅，相当于一个进程发生故障。由于议会大厅比较吵，立法者之间不能通过“演讲”传递信息，只能通过消息员在立法者之间传递信息，这也对应了分布式系统中的点对点通信。每个立法者有一个自己的法令记录册，所有立法者必须保证每个法令记录册的一致性，这对应了分布式系统中的数据一致性。</p>

<p>这篇文章的绝大部分篇幅在讲解Paxos小岛上的议会是如何从一次通过一个法令的教会发展为一次通过多个法令的兼职议会，并对每一次议会的协议提供了理论上的一致性证明，以及对议会发展过程中遇到的问题和Paxos岛民的解决方案进行阐述，文章并没有对Paxos一致性协议本身进行阐述，但我们仍然能从Paxos岛民的经验中获得一些启示。例如，他们如何定义“大多数”立法者，这个概念，起初，他们通过重量来决定，后来基于立法者的参会历史为每个立法者赋予一个“虚拟重量”，这个思想其实跟我们对分布式节点定义权重或对操作系统进程定义优先级十分相似。起初Paxos立法者每次提出一条法令，为了通过这条法令需要与其他立法者发生好几次消息传输：首先需要知道其他立法者的上一次投票法令编号，然后发送开始投票信息，接收到其他立法者投票的信息后，还需要发送是否通过法令的结果信息。消息传输的开销十分巨大，后来他们精简了消息传输，例如一条消息包含好几条法令，并且合并上一轮的结果消息和下一轮的开始消息，大大减少了消息传递次数，这一点跟我们在分布式系统中对消息进行压缩以及合并发送的思想也十分相似。</p>

<p>文章最后用短短的篇幅介绍了Paxos议会和计算机领域的状态机以及三阶段提交协议的关联，我觉得Paxos议会所包含的分布式系统设计思想还有更多的内容可以深入分析。</p>

<h2 id="著名的分布式机器学习系统"><strong>著名的分布式机器学习系统</strong></h2>

<p>这一部分的文章主要是利用传统分布式领域的思想来构建用于机器学习应用的大规模分布式系统。其中许多工作都来自产业界，已经被实际部署并验证其性能。我将从这几个分布式系统被提出的时间顺序来依次分析。</p>

<h3 id="graphlab">GraphLab</h3>

<p>CMU于2010年提出了GraphLab[5]。GraphLab是针对通用机器学习算法提出的高性能并行计算框架，通过利用机器学习算法中的稀疏结构和常见的计算模式，大大提升了机器学习算法的并行计算性能。</p>

<p>GraphLab主要由四部分组成：① 数据模型。GraphLab将计算任务抽象为一个有向数据图，以及一个共享数据表；② 用户定义的计算。包括update function和sync mechanism，分别对应MapReduce中的Map和Reduce。但跟MapReduce不同的是，update function可以访问并修改计算图中的信息，而且sync mechanism可以和update function并行执行；③ 数据一致性。包括三个程度的数据一致性，分别是full consistency, edge consistency 和 vertex consistency，用户可以根据应用的具体需要进行选择；④ 调度器。GraphLab针对不同的算法选择不同的调度器，并且允许用户自定义新的调度算法。</p>

<p>GraphLab相对于已有的分布式计算框架的优势是，可以表达有计算依赖的算法，MapReduce没有提供对迭代式算法的支持，无法实现这一点。并且MapReduce针对大型的数据中心的应用进行了非常多的优化，当我们把MapReduce应用到小型的集群或者多处理器的系统，这些优化的性能开销就变得非常大，GraphLab则显得更加轻量级。除此之外，在一些框架中，并行计算被表示为有向无环图，然而有向无环图不能表示迭代式的算法，因为图结构依赖于迭代的次数。另一种Systolic抽象，是扩展版的有向无环图，可以表达迭代式算法，但是不能表达大多数机器学习算法中的参数更新调度。</p>

<h3 id="distbelief">DistBelief</h3>

<p>Google于2012年提出了DistBelief[3]，这是TensorFlow的前身。由于GraphLab是针对通用机器学习算法提出的框架，并没有对神经网络的结构进行分析和优化，DistBelief填补了这一空缺。DistBelief虽然是针对大型神经网络的训练框架，但也能训练中等规模神经网络和任何基于梯度更新的机器学习算法。</p>

<p>这篇文章最大的亮点在于根据神经网络的性质，提出了两个分布式算法，从而提高了分布式神经网络的计算效率。</p>

<p>Downpour SGD：把数据分成几个部分，每个部分上运行模型的一个复制。模型的每个复制通过一个中心化的参数服务器交换参数更新。同时这个中心化的参数服务器是分片化的，由很多个机器组成，每个机器管理一部分的参数。因此这个算法中的异步性存在于两个层面：模型的每个复制是异步、独立运行的，参数服务器的每个分片是独立、异步更新的。这样的异步SGD也比同步SGD更容易处理分布式集群的故障问题。</p>

<p>Sandblaster L-BFGS：批量训练的算法。一个协调进程负责在参数服务器和工作节点之间调度协调，避免模型在训练时与参数服务器产生大量的传输开销。同时也解决了“掉队者”问题，通过把一个批量的工作分成更细的粒度分发给工作节点，在某台机器空闲时调度给它新的任务，这样性能好的机器就能执行更多的任务，在一个批量的任务快结束时，协调进程还会分发剩余任务的多个副本，接收最快返回的结果。这一点与MapReduce的处理机制类似。同时Sandblaster L-BFGS只需要在每个批量的任务开始前获取一次数据，结束时回传数据，比起Downpour SGD，对带宽的要求更低。</p>

<h3 id="adam">Adam</h3>

<p>在2014年的OSDI上，Microsoft和CMU分别提出了一个大规模分布式神经网络训练框架。两篇文章都在前人的工作上针对应用场景中的具体问题，做了大量的细节优化，从而提高分布式神经网络训练的速度。</p>

<p>Microsoft提出了Adam[4]，通过整个系统的协同设计，优化并平衡了计算与数据传输。同时，重构了跨节点的计算，从而减少了传输开销。并且，他们开发了机器学习训练过程中对不一致性的容忍程度，从而提高了训练的性能和扩展性。</p>

<p>Adam整个架构包括3部分：快速的数据供应、模型训练过程的优化、全局的参数服务器。他们的工作主要关注各个环节的计算和数据传输的优化。</p>

<p>在模型训练部分，他们使用了模型并行的机制，对整个神经网络进行垂直划分，他们表示这样划分模型会最小化机器间的数据传输。同时在每个机器内部，使用多线程共享权重进行训练，这一点类似数据并行，但不同于数据并行，因为多线程共享了相同的权重参数，并且通过NUMA- aware allocations来减少多线程访存的冲突。并且，他们让多个线程异步地、无锁地更新权重，这可能引入冲突、可能会更新到较老的权重，但因为神经网络是有弹性的，因此并不会极大程度上影响最终效果。他们还划分模型使其可以被放进L3 cache从而提高了访存带宽。对于掉队者，他们的处理是，对每个epoch当75%的数据处理完毕，就认为这个epoch计算完成，不再等待掉队者的计算结果，实验表明这可以加速计算过程20%并且不会影响模型精度。同时，他们还根据神经网络不同层的特点，设计了与参数服务器更新参数的不同机制，对于卷积层，由于参数量不大，可以每处理k张图片后就把参数更新发到参数服务器，对于全连接层，由于参数量巨大，可以发送梯度向量而不是参数本身，从而减少了数据传输量。</p>

<h3 id="parameter-server">Parameter Server</h3>

<p>CMU的工作[6]则更关注参数服务器的设计。他们的框架主要有5大特点：①高效的数据传输。他们使用了一个异步的数据传输模型，针对了机器学习问题进行优化，减少了数据传输的开销和带宽的占用。由于机器学习是迭代式的算法，每次迭代，需要传输的key-value 对基本是一致的，因此接收节点可以保存key list，下次发送节点不再发送key list，而只需要发送一个key list的哈希值。同时，value中可能包含大量的0，可以对消息进行压缩后再发送。② 灵活的一致性模型。他们的框架允许用户针对不同的算法选择不同的一致性模型。还允许用户自定义规则来针对某个具体的key-value对有选择性地同步。例如，只有当某个参数的改变值超过了某个临界值才进行同步。③ 有弹性的可扩展性。新的节点可以随时加入，不需要重启整个框架。④ 容错和耐用性。从机器宕机后恢复的时间很短，并且不会影响计算过程。每个工作节点组有个调度节点，针对节点的删除和加入，会调整对任务的调度。⑤易用性。全局共享参数的数据结构是向量和矩阵，这样的数据结构使得很容易应用高效的、多线程的加速库。对数据还采用了批处理的方法，即对一整个向量或矩阵的一整行进行更新，避免了过多的数据传输开销。</p>

<h3 id="chaos">CHAOS</h3>

<p>除此之外，我还看到了一篇2019年发表的文章CHAOS[7]，这篇文章是针对CPU集群设计的神经网络训练框架。目前大量的研究都是选择GPU来作为神经网络训练的加速卡，很少有人用CPU来训练大规模的神经网络。这篇文章的关注点其实不在于“分布式”，而在于“并行”，因为文中主要研究了神经网络在CPU上训练时的线程级并行和SIMD并行。这篇文章所采用的线程级并行与分布式训练环境中的工作节点间的并行类似。线程间是数据并行，每个线程独立地从数据集中取数据完成前向计算和后向更新。</p>

<p>CHAOS的全称是Controlled Hogwild with Arbitrary Order of Synchronization。Hogwild是一种异步更新的算法，Controlled Hogwild是指，每次迭代所计算出的参数更新，在本地（当前线程）立即更新，之后再与其他worker（线程）共享更新。每个worker并不等待其他worker发送的更新信息。Arbitrary Order of Synchronization是指，不进行明确的全局更新，可见其实这篇文章的核心思路就是利用神经网络对错误的容忍程度来加速其训练过程。与其他工作不同的是，这篇文章还提出了一个性能模型，用来预测不同卷积网络结构在不同epoch数量和线程数量下的性能。</p>

<h3 id="小结">小结</h3>

<p>纵观以上这几个大规模神经网络的训练框架，我最大的感受是，没有特别让人印象深刻的设计，都是针对实际应用场景进行针对性的优化，并且优化的思想大多来自分布式系统领域已有的工作，但他们能够把这些细节的优化协调统一成一个完整的框架，这其中的难度也不容小觑，这就是他们的工作最大的意义。</p>

<h2 id="分布式机器学习训练算法"><strong>分布式机器学习训练算法</strong></h2>

<p>在机器学习应用中，最常用的训练算法就是随机梯度下降（SGD），为了提高随机梯度下降算法的性能，以及让它适用于分布式的训练环境，许多学者针对这个简单的算法提出了针对分布式环境的改进。这个部分的三篇文章包含大量的理论证明，我将略去不做分析，主要分析他们在将算法扩展到分布式环境时的考虑与取舍。</p>

<h3 id="并行的sgd算法">并行的SGD算法</h3>

<p>文章[9]首次提出并行的SGD算法。其主要思路是将SGD与MapReduce结合起来，假设有k台机器，先将所有的数据分成k份，将每份数据放到一台对应的机器上。对应于MapReduce的Map过程的是，每台机器独立地在本地数据上进行随机梯度下降的计算过程，对应于Reduce过程的是，将k台机器的权重取平均，得到最终的权重。这篇文章还对这一算法的正确性提出了理论证明。这个算法的核心思想在于，通过把数据存在每个机器的本地，大大减少了I/O开销和数据传输开销，只需要在最后交换模型的参数信息。因此非常适合用MapReduce来实现。</p>

<p>这篇文章也跟另一种思路进行了对比，另一种思路是将问题划分为多个子问题，每个工作节点对一个子问题进行计算，最终将多个子问题的解进行平均，这个做法能极大程度减少数据传输，但从理论上分析，这种算法得到的结果会存在比较大的方差，影响最终结果的准确性。</p>

<h3 id="stale-synchronous-parallel算法">Stale Synchronous Parallel算法</h3>

<p>文章[14]提出了Stale Synchronous Parallel (SSP)算法，其核心思想就是利用“过期”的参数版本。对SSP算法的一个简单解释如下：假设有P个worker，每个worker都能对一个共享的变量x做更新，每个worker更新的周期是固定的，称为clock。每个worker都有自己的clock，且只会在一个clock结束时对共享变量进行更新。最新的更新不一定会立刻被其他worker看到，也就是说，当一个worker尝试去读取共享变量x的值时，读到的是过期的更新。根据这个观察，worker可以直接从本地cache读取共享变量，不需要等待从参数服务器获取共享变量最新的值。通过用户给定一个staleness 临界值，SSP模型能够最大化每个worker用在有效计算上的时间，并且提供了正确性的保证。</p>

<p>在这篇文章之前已有的算法包括BSP（Bulk Synchronous Parallel）和完全异步更新的算法。作者通过证明表示，SSP能比这两种算法都达到更快的收敛。并且，SSP不会像完全异步更新的算法那样产生大量的访存冲突，从而带来性能下降。</p>

<h3 id="中心化与去中心化算法的对比">中心化与去中心化算法的对比</h3>

<p>中心化的SGD算法往往会包括一个参数服务器，由参数服务器担任所有计算节点的中介，统一收集所有计算节点的梯度值并计算参数更新，再发送给所有计算节点。去中心化的SGD算法则依赖于类似all-reduce的操作在节点之间交换参数更新。</p>

<p>文章[13]从理论上分析、对比了中心化SGD和去中心化SGD。这篇文章指出，使用中心化还是去中心化SGD，取决于很多因素，包括：网络拓扑、带宽、延迟、参数更新频率、容错机制等等。但大部分人都有一种观点，认为去中心化SGD只是在不能使用中心化SGD的情况下的一种妥协，其性能不如中心化SGD，这篇文章指出在某些情况下去中心化算法能达到比中心化算法更好的性能。作者从理论上分析了去中心化算法的可扩展性，发现当更多的节点可用时，去中心化算法可以实现近似线性加速。并且作者在多种机器学习框架和网络上进行了实验，发现去中心化算法在低带宽和高延迟的情况下，能实现相比中心化算法10倍的加速。</p>

<h2 id="机器学习与系统结合的文章"><strong>机器学习与系统结合的文章</strong></h2>

<p>这一部分主要包括几篇不能被分到前几类的文章，它们也基本属于机器学习和系统相结合的工作。</p>

<h3 id="利用gpu实现加速">利用GPU实现加速</h3>

<p>文章[8]发表于2009年，可能是最早提出用GPU来训练机器学习模型的文章之一。当时大多数工作都还停留于在CPU上训练机器学习模型，并且NVIDIA的生态也还不完善，没有cuDNN这样的针对GPU平台的算法加速库。因此，这篇文章提出用GPU来训练机器学习模型就显得很有前瞻性。这篇文章用到的例子是深度置信网络（DBN）和稀疏编码问题。作者表示，只要使用足够多的训练数据，比较简单的模型也能得到比复杂模型更好的效果。因此提出使用GPU在大规模数据集上训练简单模型来提高模型的精度，同时加速训练过程。</p>

<p>GPU往往有数百个处理单元，并且峰值访存带宽也高于CPU。GPU能并发计算数千个线程，并且调度开销非常小，这样的属性使其很适合作为一个通用的并行计算框架。然而GPU虽然有着非常好的并行特性，但却无法像CPU那样执行通用处理器的程序，因此，这篇文章对机器学习算法进行了一定程度上的重新设计，使其更适合在GPU上运行。</p>

<p>这篇文章的核心思想在于，对于机器学习应用，把所有的参数存在GPU的global memory中，能极大减少数据传输的开销。而训练数据往往不能存在global memory中，但可以按chunk进行分组，减少传输的次数。并且，在当时，已有工作尝试用MapReduce来实现分布式的机器学习算法，但这样的工作也只开发到了数据并行这个级别，而GPU提供的两个层级的架构，可以在block层面用数据并行，而thread层面使用更细粒度的并行。</p>

<h3 id="利用mapreduce实现可扩展性">利用MapReduce实现可扩展性</h3>

<p>文章[10]认为，大量的研究工作都使用GPU作为加速卡，但使用GPU很难实现可扩展性，因为GPU的内存有限，而使用MapReduce来并行，就可以利用MapReduce框架内在拥有的数据带宽，实现可扩展性。这篇文章提出了使用MapReduce来训练深度置信网络（DBN）的一种实现。</p>

<p>基于MapReduce框架设计分布式的DBN训练算法，核心在于设计一个Map函数和Reduce函数，以及恰当的key-value对。由于DBN是由多个有限玻尔兹曼机（RBM）堆叠组成的，这篇文章对DBN的训练，包括了对多个RBM的分布式预训练，以及对整个DBN的分布式反向传播训练算法。对每个RBM进行分布式训练时，每个epoch都有一组Map节点，每个Map节点负责一部分数据，计算出参数的更新值后通过key-values发送给reduce节点，再由reduce节点执行参数的更新过程。对DBN的训练也是类似的，Map执行前向计算过程，Reduce执行反向更新过程。这篇文章可以看做是将MapReduce在机器学习场景的一次具体应用，将具体问题和已有框架相结合，没有太多的创新点。</p>

<h3 id="对神经网络的细粒度划分">对神经网络的细粒度划分</h3>

<p>文章[11]关注的问题是对卷积神经网络进行细粒度的划分，从而提高卷积网络的可扩展性。在此之前，大多数卷积网络主要关注小型的图片输入，例如ImageNet中的每个图片大小为224x224x3，很多网络因此针对这个量级的图片而设计，在进行并行训练时也都基本采用了数据并行，每个计算节点保存一部分数据。但逐渐出现了一些比较大的图片集，使得数据并行不那么有效了，因为单张图片比较大，如果采用数据并行，每个计算节点能保存的数据量就比较小，从而无法实现比较大的batch size 的训练。因此这篇文章提出对卷积网络中的节点进行空间划分，即每个节点处理模型输入数据在空间维度一部分，对于图片数据，就是处理图片的一个子区域。</p>

<p>除此之外，这篇文章还提出了对卷积网络运行时间进行建模的方案，并且将寻找网络最优划分的问题转化为一个图问题，利用启发式规则对每个卷积层生成一些候选划分方式，再使用性能模型对每种划分方式进行时间预测，最后使用图算法寻找最短路径的方式，找到网络的最佳划分方案。</p>

<h3 id="对gpu程序访存模式的研究">对GPU程序访存模式的研究</h3>

<p>文章[12] 指出越来越多的计算任务在多GPU节点上进行，为了能充分利用GPU之间的低延迟和高带宽，程序员往往需要对程序进行精细的划分，充分考虑负载平衡，边界条件以及设备间同步等等问题。这篇文章提出了MAPS-Multi，一个自动的多GPU划分框架，可以根据任务访存的模式把任务划分到多个GPU上。这篇文章在真实应用，例如机器学习上，实现了接近线性的加速比。这篇文章提出了对GPU上的任务其输入和输出访存模式的不同分类，然后实现了一个框架，可以根据这些访存模式，对GPU 核函数进行自动的划分，以及边界值的交换，使得程序员不需要关注底层的细节，在多GPU环境下的编程更加简单。</p>

<h3 id="层级化的参数服务器以及对深度学习性能指标的分析">层级化的参数服务器以及对深度学习性能指标的分析</h3>

<p>文章[15] 提出了一个基于参数服务器的分布式深度学习框架，Rudra。并且通过在这个框架中一系列的实验，系统地分析了深度学习问题中两大重要因素，运行时间和模型准确率之间的权衡。分析了同步策略, 陈旧的参数更新, 批大小, 学习率,以及工作节点数量对这两个因素的影响。这篇文章提出了一种动态调整学习率的方式，可以实现模型更快的收敛，提出了一种新的同步机制，可以减少梯度过期对模型准确率的影响，并且文章指出，当工作节点数量增加时，为了保持模型的准确率，要减少minibatch size。</p>

<p>除此之外，这篇文章提出的层级化参数服务器结构Rudra-adv，也十分新颖。通过将参数服务器组织为树狀层级结构，可以减少数据传输开销。为了进一步减少数据传输开销，作者又提出了Rudra-adv* ,直接在所有的训练节点之内构造这个层级化的参数服务器，进一步利用了训练节点之间的连接和带宽。</p>

<h2 id="总结"><strong>总结</strong></h2>

<p>纵观这20篇文章，我的感受是，分布式系统领域那些许多年前被提出的算法，至今仍然在影响着学术界和工业界对系统的设计。例如在Chubby的设计中，Chubby cell内部的server之间仍然是使用类似Paxos的一致性协议来选举Master server。例如Paxos小岛上那种压缩信息量和减少消息传递次数的思想，仍然能在Adam和CMU的机器学习框架中看到。</p>

<p>当然，对系统设计的权衡也在随着具体场景的变化逐渐发生改变，Lamport在论文中详细刻画了系统的全序关系以及逻辑时钟，研究如何确保分布式系统的一致性，而机器学习领域的研究者对一致性逐渐没有那么苛求，他们发现，容忍一定程度的不一致性反而能带来性能的提升。又比如，在MapReduce这样传统的框架中，为了解决掉队者的问题，他们会把掉队者的任务再次调度给其他节点，力争在保证程序正确性的前提下提高性能，而机器学习科学家们对掉队的节点也没有那么苛责了，他们直接放弃那些掉队节点的结果，发现也能在不损失模型精度的前提下提高性能。</p>

<p>总的来说，我认为，要进行系统领域的研究，吃透这些经典论文的思想是一个前提条件，分布式系统中许多共通的特点可以使我们在设计一个系统时更快地解决那些前人已经分析透彻的问题，而着眼于具体应用场景的特点，设计因地制宜和与时俱进的策略。</p>

<h2 id="参考文献">参考文献</h2>

<p>[1] Dean, Jeffrey &amp; Ghemawat, Sanjay. (2004). <strong>MapReduce: Simplified Data Processing on Large Clusters.</strong> Communications of the ACM. 51. 137-150. 10.1145/1327452.1327492.<br />
[2] Zaharia, M. , Chowdhury, M. , Das, T. , Dave, A. , &amp; Stoica, I. . (2012). <strong>Resilient distributed datasets: A fault-tolerant abstraction for in-memory cluster computing.</strong> Proceedings of the 9th USENIX conference on Networked Systems Design and Implementation. USENIX Association.<br />
[3] J. Dean et al. 2012. <strong>Large scale distributed deep networks.</strong> In Proceedings of the 25th International Conference on Neural Information Processing Systems (NIPS’12), vol. 1. 1223–1231.<br />
[4] T. Chilimbi, Y. Suzue, J. Apacible, and K. Kalyanaraman. 2014. <strong>Project adam: Building an efficient and scalable deep learning training system.</strong> In Proceedings of the 11th USENIX Symposium on Operating Systems Design and Implementation. 571–582.<br />
[5] Yucheng Low, Joseph Gonzalez, Aapo Kyrola, Danny Bickson, Carlos Guestrin, and Joseph Hellerstein. 2010. <strong>GraphLab: a new framework for parallel machine learning.</strong> In Proceedings of the Twenty-Sixth Conference on Uncertainty in Artificial Intelligence (UAI’10). AUAI Press, Arlington, Virginia, USA, 340–349.<br />
[6] M. Li et al. 2014. <strong>Scaling distributed machine learning with the parameter server.</strong> In Proceedings of the 11th USENIX Conference on Operating Systems Design and Implementation (OSDI’14). 583–598.<br />
[7] A. Viebke, S. Memeti, S. Pllana, and A. Abraham. 2019. <strong>CHAOS: A parallelization scheme for training convolutional neural networks on Intel Xeon Phi.</strong> The Journal of Supercomputing 75, 1 (Jan. 2019), 197–227.<br />
[8] R. Raina, A. Madhavan, and A. Y. Ng. 2009. <strong>Large-scale deep unsupervised learning using graphics processors.</strong> In Proceedings of the 26th Annual International Conference on Machine Learning (ICML’09). 873–880.<br />
[9] M. A. Zinkevich, M. Weimer, A. Smola, and L. Li. 2010. <strong>Parallelized stochastic gradient descent.</strong> In Proceedings of the 23rd International Conference on Neural Information Processing Systems, vol. 2. 2595–2603.<br />
[10] K. Zhang and X. W. Chen. 2014. <strong>Large-scale deep belief nets with MapReduce.</strong> IEEE Access 2 (2014), 395–403.<br />
[11] N. Dryden et al. 2019. <strong>Improving strong-scaling of CNN training by exploiting finer-grained parallelism.</strong> In Proceedings of the 33rd IEEE Int:l Parallel &amp; Distributed Processing Symposium (IPDPS’19).<br />
[12] T. Ben-Nun, E. Levy, A. Barak, and E. Rubin. 2015. <strong>Memory access patterns: The missing piece of the multi-GPU puzzle.</strong> In Proceedings of the International Conference for High Performance Computing, Networking, Storage and Analysis (SC’15). 19:1–19:12.<br />
[13] X. Lian et al. 2017. <strong>Can decentralized algorithms outperform centralized algorithms? a case study for decentralized parallel stochastic gradient descent.</strong> In Advances in Neural Information Processing Systems 30. MIT Press, 5336–5346.<br />
[14] Q. Ho et al. 2013. <strong>More effective distributed ML via a stale synchronous parallel parameter server.</strong> In Proceedings of the 26th International Conference on Neural Information Processing Systems, vol. 1 (NIPS’13). 1223–1231.<br />
[15] S. Gupta, W. Zhang, and F. Wang. 2016. <strong>Model accuracy and runtime tradeoff in distributed deep learning: A systematic study.</strong> In Proceedings of the IEEE 16th International Conference on Data Mining (ICDM’16). 171–180.<br />
[16] Burrows, M. (2006, November). <strong>The Chubby lock service for loosely-coupled distributed systems.</strong> In Proceedings of the 7th symposium on Operating systems design and implementation (pp. 335-350).<br />
[17] Fetterly, Y. Y. M. I. D., Budiu, M., Erlingsson, Ú., &amp; Currey, P. K. G. J. (2009). <strong>DryadLINQ: A system for general-purpose distributed data-parallel computing using a high-level language.</strong> Proc. LSDS-IR, 8.<br />
[18] DeCandia, G., Hastorun, D., Jampani, M., Kakulapati, G., Lakshman, A., Pilchin, A., … &amp; Vogels, W. (2007). <strong>Dynamo: amazon’s highly available key-value store.</strong> ACM SIGOPS operating systems review, 41(6), 205-220.<br />
[19] Lamport, L. (2019). <strong>Time, clocks, and the ordering of events in a distributed system.</strong> In Concurrency: the Works of Leslie Lamport (pp. 179-196).<br />
[20] Lamport, L. (2019). <strong>The part-time parliament.</strong> In Concurrency: the Works of Leslie Lamport (pp. 277-317).</p>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="Distributed System" /><category term="Deep Learning" /><summary type="html"><![CDATA[摘要 我选择的20篇文章主要关注分布式系统，以及分布式系统在机器学习中的应用。分布式系统是计算机科学历史悠久的一个研究方向，设计一个分布式系统需要考虑许多方面的因素，例如进程的交互与通信协议、故障处理及容错机制、数据复制及一致性等。经过学界多年的研究，针对这些问题，已经有了比较好的解决方案。而在当下，机器学习正迅速发展，越来越多的数据以及规模越来越大的模型，都使得在分布式环境中训练机器学习模型成为一大主流趋势。如何针对机器学习应用设计分布式系统，从而加速机器学习应用的训练过程，成为现在研究的热潮。]]></summary></entry><entry><title type="html">分布式深度学习-Survey</title><link href="https://vamix.github.io/posts/2020/04/dist-deep-learning-systems/" rel="alternate" type="text/html" title="分布式深度学习-Survey" /><published>2020-04-30T00:00:00-07:00</published><updated>2020-04-30T00:00:00-07:00</updated><id>https://vamix.github.io/posts/2020/04/blog-post-1</id><content type="html" xml:base="https://vamix.github.io/posts/2020/04/dist-deep-learning-systems/"><![CDATA[<h2 id="intro">Intro</h2>

<p>由于神经网络的规模越来越大，单个计算设备往往难以在短时间内完成对整个网络的训练，因此出现了大量的分布式神经网络训练方案，利用多个节点的计算能力加速训练过程。 而计算设备也在不断更新，可以是CPU、GPU以及TPU等更多类型的硬件。本文调研了分布式深度学习领域相关的最新文献，简要介绍该领域目前关注的主要问题和已有的解决方案。<!--more--> 文章的内容安排如下：第2节简要介绍背景知识，包括机器学习和深度神经网络的基本概念、SGD算法的细节；第3节介绍深度神经网络的性能模型；第4到7节分别介绍图14所示三种分布式训练的并行方案：数据并行、模型并行和流水线并行，以及结合多种方案的混合并行。第8节介绍寻找最优并行策略一般采用的算法；第9节介绍对数据传输过程进行优化的主要方法。本文图片及2、3、4、9节部分内容，参考自<strong>[81]</strong>。</p>

<p><img src="/images/DistributedML/003.png" /></p>

<h2 id="background">Background</h2>

<h3 id="机器学习和深度神经网络">机器学习和深度神经网络</h3>

<p>这一节将简要介绍机器学习的相关概念，以及深度神经网络和机器学习的关系。</p>

<p>机器学习可以分为监督学习、无监督学习、半监督学习等。<strong>监督学习</strong>是指从<strong>标注数据</strong>中学习预测模型的问题。 标注数据表示输入输出的对应关系，预测模型对给定的输入产生相应的输出。<strong>监督学习的本质是学习输入到输出的映射</strong>的统计规律。<strong>无监督学习</strong>则是从无标注数据中学习一定模式的问题。<strong>半监督学习</strong>则是采用一部分标注数据和一部分无标注数据进行学习的问题。我们经常看到的图片分类、语音识别等，大多都是监督学习问题。</p>

<p>监督学习问题的一般框架如下：</p>

<ul>
  <li>
    <p><strong>输入</strong>：n个独立同分布（i.i.d.）的训练数据$(x_i,y_i)\in \mathcal{X} \times \mathcal{Y}, i=1,…,n$。 在回归问题中，$Y$是连续的；在分类问题中，$Y$是离散的。例如对于图片分类问题，$ \mathcal{X}$ 可以是32x32的图片的集合，$ \mathcal{Y}$ 可以是标注图片类别的标签。</p>
  </li>
  <li><strong>目标函数</strong>：$f \in F$。一个机器学习模型，可以表示为一个目标函数$f$，带有一些参数，用$\theta$表示，$\theta \in \mathbb{R}^d$。</li>
  <li><strong>损失函数</strong>：$L(f;x,y)$ 。常用的损失函数有0-1损失函数、平方损失函数、绝对损失函数、对数损失函数等。我们希望损失函数是可导的，以便于对参数进行优化。</li>
  <li>
    <p><strong>期望风险（expected risk）</strong>：$R_{exp}(f)=\int L(f;x,y)dP(x,y)$。这是理论上模型$f(X)$关于联合分布$P(X,Y)$的平均意义下的损失。与此相对的，存在一个经验风险（empirical risk）：$R_{emp}(f)=\frac{1}{N}\sum_{i=1}^{N}L(y_i,f(x_i))$。经验风险是在训练数据集上得到的平均损失。根据大数定理，当样本容量N趋于无穷时，经验风险$R_{emp}(f)$趋于期望风险$R_{exp}(f)$。</p>
  </li>
  <li><strong>学习任务（task）</strong>：数据域$\mathcal{X} \times \mathcal{Y}$上的数据分布$\mathcal{D}$，带有参数$\theta$的目标函数$f$，损失函数$L(f;x,y)$共同构成了一个学习任务。我们的目标就是找到某个参数$\theta$，使得期望风险$R_{exp}(f)$尽可能小。由于我们往往只能得到n个训练数据，根据大数定理，我们一般求解经验风险，作为期望风险的预测值。</li>
</ul>

<p>神经网络是模拟生物大脑结构和功能而产生的一种计算模型，可以看做是以上框架中目标函数的一种。神经网络由大量神经元连接构成，每个神经元就是一个简单的函数，对输入数据进行处理后传输给下一个神经元。近年来，神经网络的深度不断增加，使其可以表达更加复杂的层次化信息，在越来越多的领域得到广泛应用。</p>

<h3 id="随机梯度下降算法-sgd">随机梯度下降算法 SGD</h3>

<p>对神经网络进行训练的最常用算法是随机梯度下降算法（SGD）<strong>[1]</strong>。</p>

<p><img src="/images/DistributedML/007.png" /></p>

<p>第一行规定了运行SGD的次数，即终止条件或computational budget。一般来说，实际的机器学习应用的终止条件可能是固定的迭代次数、固定的运行时间，或达到某个准确率。第二行是从数据集$S$中随机采样<strong>一个</strong>样本。第三行是计算损失函数$l$在参数$w^{(t)}$下的梯度。上标$t$代表第$t$次迭代。每一层（例如第$i$层）的梯度$w_{i}^{(t)}$是通过反向传播得到的。第四行是用某种权重更新规则$u$，根据得到的梯度值以及之前的权重值来更新权重。</p>

<h3 id="权重更新规则">权重更新规则</h3>

<p><img src="/images/DistributedML/008.png" /></p>

<p>权重更新规则可以被定义为梯度$g$、之前的权重$w^{(0)},…,w^{(t)}$以及当前迭代次数$t$的函数。主要有以下几种：</p>

<ul>
  <li>Learning Rate。最基本的SGD update rule。$\eta$ 代表学习率，控制着梯度值对下一轮权重参数的影响程度。</li>
  <li>Adaptive Learning Rate。通常学习率被设置为固定值，但有时候也会设置一个跟迭代次数相关的学习率$\eta_t$ ，随着迭代次数逐渐降低。</li>
  <li>Momentum，动量法。使用当前权重和过去权重的差值$w^{(t)} - w^{(t-1)}$ 来避免局部最小和多余步骤。</li>
  <li>RMSProp、Adam等。使用梯度的一阶矩、二阶矩来适应每个权重参数的学习率，增强了稀疏更新的能力。</li>
  <li>表格中的所有系数都被称作超参数。为了得到最好的结果，超参数是需要根据具体应用进行调整的。</li>
</ul>

<h3 id="minibatch-sgd">Minibatch SGD</h3>

<p><img src="/images/DistributedML/009.png" /></p>

<p>实际使用SGD时，更常用的是Minibatch SGD，从数据集的<strong>一个子集</strong>（而不是一个样本）中得到梯度的平均值。Minibatch SGD代表了SGD（一次计算一个样本）和batch GD（一次计算整个数据集）之间的tradeoff。</p>

<p>Minibatch SGD的实现方法是，随机打乱数据集，每次处理连续的B个样本，完整遍历数据集一次被称作一个<strong>epoch</strong>。</p>

<h3 id="generalization-vs-utilization">Generalization vs Utilization</h3>

<p><img src="/images/DistributedML/013.png" width="300" align="center" /></p>

<p>Generalization指的是模型的准确率，Utilization指的是硬件的利用率。如何设置minibatch size是一个复杂的问题，因为它不仅会影响模型的准确率，也会影响硬件的利用率。</p>

<p>如图所示，minibatch size不能太小（区域A），太小的minibatch size不仅会导致很低的硬件利用率，也会产生较大的Validation error。也不能太大（区域C），当minibatch size增加至超过某个界限后，硬件利用率并无明显增加，而Validation error却会增加。</p>

<h2 id="性能模型-performance-model">性能模型 Performance Model</h2>

<p><img src="/images/DistributedML/001.png" /></p>

<p>要研究分布式深度学习，很多时候我们都需要一个深度神经网络的性能模型。有了性能模型，就能指导我们进行网络的设计、分布式部署方案的设计、寻找最优的模型划分策略等等。但设计一个神经网络的性能模型并不容易。</p>

<p>最大的难点在于，<strong>对组成神经网络的op进行时间建模</strong>。因为op的性能模型往往是非线性的。大多数op都会调用低层加速库中实现好的kernel，图11是CUBLAS库中矩阵乘的数据<strong>[2]</strong>，可以看出，运算时间并不随数据大小线性改变，因为CUBLAS会从15种矩阵乘的实现中，根据具体的矩阵大小，选择性能最好的一种。</p>

<p>对深度神经网络的时间进行建模，主要关注以下几类运算时间占比最高的op：对于卷积网络，是全连接层和卷积层，对于循环网络，是循环单元。其中，卷积层的建模尤其复杂。</p>

<h3 id="全连接层">全连接层</h3>

<p>全连接层可以被看做是矩阵-矩阵乘。因此，高效的线性代数库都可被使用，例如CUBLAS、MKL、ESSL。BLAS GEMM还可在批处理时被使用。</p>

<h3 id="卷积层">卷积层</h3>

<p>卷积可以被直接计算，但这会使得在向量处理器或多核架构下的硬件利用率极低，这些硬件大多都有对并行的乘法累加运算进行优化。<strong>文章[3]</strong>指出，如果不对卷积算法本身进行改进，可以通过对运算进行重排序或引入冗余数据来最大化数据的重用率，从而提高硬件利用率，这篇文章还分析了卷积和池化op的数据传输量的下界，并进一步分析了怎样才能通过循环的重新组织和tiling达到下界。</p>

<p>但比较常见的卷积层优化，是从卷积算法本身来进行的。最常见的三种对卷积算法的改进：</p>

<p><img src="/images/DistributedML/002.png" /></p>

<p>图12(a)是方案1，把卷积操作转化为矩阵乘法（通过使用Toeplitz 矩阵, 这一过程也叫做im2col），然后采用GEMM。其具体操作是，把输入的图像从3D tensor转化为2D矩阵，矩阵中的每一行都是被展开的要被卷积的一个patch，（这样的转化会产生一些数据冗余，因为被多次卷积的数据会被多次展开）；把多个卷积核存储为一个2D矩阵，矩阵的每一列都是一个被展开的卷积核，把两个矩阵相乘后的结果再转化成3D tensor，就是卷积后的结果。还有另一种与im2col对应的方案叫做kn2row<strong>[3]</strong>。由于上述方案会消耗大量的内存（因为要保存转化后的矩阵），cuDNN在实际实现中提供了implict_GEMM和precomp_GEMM。在implicit_GEMM中，Toeplitz矩阵并不会真的实例化。也有<strong>文章[4]</strong>使用Strassen矩阵乘法来进行优化。</p>

<p>图12(b)是方案2，FFT。<strong>文章[5]</strong>指出，卷积核越大，FFT的收益就越大。除此之外，也能通过一些DNN特有的性质来对FFT算法进行改进，从而在DNN上实现更大的加速。文章引入两种FFT卷积的实现，分别基于cuFFT（闭源）和fbfft（自主实现，开源）。最后实现的性能比NVIDIA的cuDNN中的实现更好。
（因为这篇文章发表的时间较早（2015），尚不确定最新的cuDNN是否已对fft进行改进。）</p>

<p>图12(c)是方案3，Winograd算法<strong>[6]</strong>。因为FFT适用于大的卷积核，因此这篇文章使用Winograd算法提出了一种适用于小卷积核的卷积算法。而且，由于在Winograd算法中，运算次数随着卷积核大小的增长而平方增长，该方法也只适用于小的卷积核。</p>

<p><strong>文章[5] [6] [7]</strong>的实验结果表明，并没有“one-size-fits-all” 的卷积算法。</p>

<p>数据的布局（layout）对性能的影响也很大。<strong>文章[8]</strong> 指出把数据从$N×C×H×W$ tensors 转化为$C ×H ×W ×N $ tensors，卷积和池化操作会被计算地更快。</p>

<p>DNN加速库，例如cuDNN、MKL-DNN提供了多种卷积算法和数据布局方案，并且提供了一些函数来帮助用户在给定tensor大小和内存限制的条件下选择最佳算法。在实现时，这些函数可能将每一种算法跑一遍，进而选择性能最好的。</p>

<h3 id="循环单元recurrent-units">循环单元（recurrent units）</h3>

<p>在RNN units内部往往有各种复杂的门结构，包含一些矩阵运算或element-wise操作。RNN的并行可以分为两大类：层内的并行、层间的并行。</p>

<p>层内的并行：<strong>文章[9]</strong>给出了几种针对RNN的优化。（1）把RNN units内部所有的计算融合成一个function(kernel) ，把中间结果保存在scratch-pad memory中，不仅减少了kernel调度的开销，也节省了到global memory中取数据的次数。（2）矩阵的预转换（pre-transposition）（3）在GPU的不同处理器上并发执行独立的循环单元。</p>

<p>层间的并行可以通过pipeline来实现。</p>

<p>其他的对于RNN的优化还有：从内存开销的角度进行优化，<strong>文章[10]</strong>提出用动态规划来平衡缓存中间结果和重新计算。<strong>文章[11]</strong> 在卷积op上实现了类似的效果。<strong>文章[12]</strong>提出了Persistent RNN，解决了提高GPU利用率的两个限制：小的 minibatch sizes和长序列输入。这个方案也减少了RNN的内存占用，使得用户可以搭建更深的RNN网络。</p>

<h3 id="网络总时间建模的相关文章">网络总时间建模的相关文章</h3>

<p><strong>文章[13]</strong>提出的Performance model为：神经网络总的计算时间=各层时间之和，每层时间=前向计算时间+后向计算时间+权重更新时间。假设每个layer均匀分布在多个设备之间，取时间最长的segment的时间为该layer的时间。每一个时间又分为计算时间+数据传输时间。在对前向计算时间和后向计算（梯度计算）进行建模时，关注layer之中的神经元数量，通过神经元数量估算乘法次数，再根据单次乘法时间计算总时间。</p>

<p>文中分别对模型并行和数据并行进行了建模。</p>

<ul>
  <li>
    <p>模型并行
对神经网络layer $l$ 的segment $p$ 的前向计算时间建模为：</p>

    <p><img src="/images/DistributedML/015.png" width="320" align="center" /></p>

    <p>$C_{muladd}$ 是一次乘加操作的时间。$W(l,p)$是从layer $l-1$  连接到layer $l$ 中的神经元的所有weight的数量。$C_{act}$ 是在神经元上执行的非线性函数$f(x)$的时间。$N_{neuron}(l,p)$ 是layer $l$ 的 segment $p$ 中的神经元数量。</p>

    <p>对数据传输的时间建模为：</p>

    <p><img src="/images/DistributedML/016.png" width="270" align="center" /></p>

    <p>$C_{ncost}$ 是网络的延迟。$C_{nbw}$ 是带宽。$A(l,p)$ 是segment $p$ 从layer $l-1$ 接收到的activation的数量，$C_{bit}$是activation的大小，两项相乘就是接收到的总数据量。对于传输时间的建模比较trivial。对于前向计算时间的建模过于理论化，忽略了在实际实现时的不同算法的区别。</p>

    <p>对反向传播的时间建模为：</p>

    <p><img src="/images/DistributedML/017.png" width="400" align="center" /></p>

    <p>$W^{‘}(l,p)$是从layer $l+1$ 连到layer $l$ 的连接数量。$C_{err}$ 是error function 的计算时间。（个人理解为求导时间。）</p>

    <p>对参数更新时间的建模：</p>

    <p><img src="/images/DistributedML/018.png" width="250" align="center" /></p>
  </li>
  <li>
    <p>数据并行
数据并行又分为三种模式。</p>

    <ul>
      <li>
        <p><em>Chip-level Multiprocessing</em>。定义interference factor $C_{interf} (H(l))$ 来表示多线程之间的干扰（例如访存带宽竞争）。通过运行一个小型的benchmark来估算干扰因子。最终的时间估计值就是用干扰因子乘以单线程的时间。</p>
      </li>
      <li>
        <p><em>Layer Replication</em></p>
      </li>
      <li>
        <p><em>Multiple Model Replicas - Parameter Server</em>。主要的变化在于引入了Parameter Server。写更新到PS可以异步执行，但是读出最新的参数是同步的。通过建模最坏情况下的时间（所有worker同时使用带宽）和最好情况下的时间（只有一个worker使用带宽），实际的实现中，真实时间介于二者之间，因为不同的实现方式的具体的硬件条件会导致数据传输和计算过程不同程度的overlap，使得总时间难以准确估计。</p>
      </li>
    </ul>
  </li>
</ul>

<p><strong>文章[2]</strong>是对异步的SGD进行时间建模。这篇文章指出，上述文章<strong>[13]</strong>不能应用于非PS架构的分布式环境（例如采用all-reduce在节点之间进行数据同步），也不能应用于异步的SGD。文章<strong>[2]</strong>提出的Performance model不仅能预测一个epoch的训练时间，也能预测异步SGD训练过程中的mini-batch size和staleness。</p>

<p>这篇文章对时间的建模的方法是：先构建经验模型，然后通过实际测得的时间和最小二乘法对经验模型中的未知系数进行拟合。不同于其他工作中通过硬件参数和浮点运算次数等信息来对运行时间做预测，这篇文章使用了经验模型+拟合的方法。但是这篇文章的Performance Model不准确，因为他们假设了卷积核的大小为3x3，忽略了其他大小的卷积核。</p>

<p>对前向计算和后向更新的时间建模，是通过对执行其计算过程的底层CUDA kernel和SGEMM kernel的时间进行建模。对CUDA库中的im2col的建模如下：</p>

<p><img src="/images/DistributedML/020.png" width="400" align="center" /></p>

<p>cublasSgemm的建模并不trivial，因为作者发现cublasSgemm矩阵乘法的Performance并不是随着输入矩阵大小连续变化的。这可能是因为sub-kernels根据特殊的参数进行了特殊的优化。于是这篇文章先测得大量的cublasSgemm的矩阵乘数据，包含的矩阵大小为$(a^x, a^y, a^z)$ (表示$a^x\times a^z$ 和$a^z \times a^y$ 两个矩阵相乘)，其中$a$是一个常数，$x,y,z = 0,1,2…$。接下来构建许多个线性模型，对于 $m\times k$ 和 $k\times n$ 的矩阵乘法，线性模型包含的项为：${mnk, mn, mk, nk, m, n, k}$， 以及浮点运算次数($O(mnk)$) 和最小访存次数 ($O(mn + mk + nk)$)。在拟合线性模型中的未知参数时，使用$\log m,n,k$ 空间中的8个邻居点。因此每个线性模型都有一个有效范围，在针对具体某个矩阵乘进行预测时，使用有效范围包含该测试点的线性模型。之所以要构造很多个线性模型是因为单个线性模型不能拟合出cublasSgemm的non-trivial规律。</p>

<p><strong>Paleo[14]</strong>构建了更为详细的Performance model。其总体示意图如下：</p>

<p><img src="/images/DistributedML/021.png" width="740" /></p>

<p>总时间由计算时间和数据传输时间组成。影响计算时间的因素又包括：输入数据的大小（由网络结构决定）、网络各个layer实现时所采用的算法的复杂度、硬件参数等。数据传输时间则跟网络层与层之间的依赖关系、硬件带宽和并行策略等有关。</p>

<p>我们最关心的是对卷积层的建模。其对卷积核的建模分为矩阵乘和FFT两种，这篇文章分别从理论层面估算两种方法各所需的浮点运算次数，再根据硬件的实际浮点运算速度，相除得到预测时间。Paleo通过一个offline benchmark进行测试，定义一个Heuristic来在两种算法之间进行选择。</p>

<p>他们的框架需要输入网络的参数以及硬件的各项参数。由于实际的硬件性能并不总是能达到峰值，他们还提出了一个指标：Platform Percent of Peak（PPP），通过一个小型的benchmark找到当前硬件性能占峰值的比例系数，再用Performance model乘以这个系数，即可得到实际的性能预测值。</p>

<p><strong>NeuralPower[15]</strong>团队的工作是基于Paleo来做的，他们对神经网络时间的建模侧重于推理阶段而不是训练阶段。但他们没有提出一个基于分析的理论模型，而是使用了基于学习的多项式回归方法。对神经网络op的各项参数，例如输入数据大小、输出大小、卷积核大小、步长等，结合另一些硬件参数，例如访存次数、浮点运算次数等，直接构建一个多项式模型，再使用Lasso回归进行模型拟合。NeuralPower比Paleo更加通用。但模型拟合的时间开销较大。</p>

<p><strong>文章[16]</strong>直接用一个神经网络来预测其他神经网络的运算时间。<strong>文章[17]</strong>通过浮点操作次数来构建了Performance model，并使用performance model选择满足精度要求的性能最好的网络来满足用户的实时性要求。</p>

<p>以上文章基本都是着眼于对单个layer的时间进行建模，以及对分布式环境下的数据传输时间进行建模，对于网络总时间的建模方法往往比较简单，一般是把网络总时间看成各个layer的时间之和。但是对于更加复杂的分布式环境，这样的建模方法不一定准确。FlexFlow<strong>[53]</strong>对于这个问题有一个比较好的解决方案。</p>

<p>FlexFlow为了对某种划分方式下的网络运行总时间做预测，设计了一个task graph。他们把神经网络中每个op抽象为一个task（如果op被划分成几个部分，就抽象为几个task），把节点之间的数据传输也抽象为task。task就是task graph中的node，而task graph中的edge，代表了op之间的依赖关系。</p>

<p>每个task包含几项数据：<code class="language-plaintext highlighter-rouge">exeTime</code>  <code class="language-plaintext highlighter-rouge">readyTime</code> <code class="language-plaintext highlighter-rouge">startTime</code> <code class="language-plaintext highlighter-rouge">endTime</code>等，<code class="language-plaintext highlighter-rouge">exeTime</code>是该task的执行时间。对于数据传输task，<code class="language-plaintext highlighter-rouge">exeTime</code>是数据传输时间。FlexFlow在构造任务图时，使用一个类似Dijkstra的最短路径算法，来给每个task的其他数据进行赋值。Task准备好时（所有前序任务已经结束），就会被enqueue到一个priority queue，并且会按照<code class="language-plaintext highlighter-rouge">readyTime</code>的顺序进行dequeue。类似于Dijkstra算法中，每次将被搜索到的节点的邻居节点加入边缘集合的操作。通过这样的方式，最后一个task被dequeue时，就能得到整个任务图的运行时间。</p>

<ul>
  <li><strong>对Performance Model的benchmark的设计</strong>
    <ul>
      <li>文章[16]：测试TensorFlow中的<code class="language-plaintext highlighter-rouge">tensorflow.layers.conv2d </code> op，使用不同的参数组合。参数的范围是：batch_size = [1, 64], in_wid = [1, 512], kernel_size = [1, 7], 以及多种stride和padding，以及是否存在bias。总共10,038,745,006,080种组合，从中任意选择50000组数据作为训练数据，80%作为训练集，10%作为测试集，10%作为验证集。</li>
      <li>文章[15]：AlexNet, VGG-16, VGG-19, R-CNN, NIN, CaffeNet, GoogleNet, overfeat and so on. 总计858 个卷积op, 216池化op, and 116全连接op</li>
      <li>文章[14]：AlexNet, VGG.</li>
      <li>文章[2]：自己构造的网络CNN-A, CNN-B, CNN-C .</li>
      <li>文章（Performance Evaluation）：AlexNet, VGG-19, GoogleNet, Resnet50, SqueezeNet.</li>
    </ul>
  </li>
</ul>

<h2 id="数据并行">数据并行</h2>

<p>数据并行是最简单直观的并行方案，实现起来难度最小。其核心思想是，在多个计算设备上部署相同的完整神经网络，每个设备利用训练数据的一部分进行训练并计算梯度，在每个batch的数据计算完成后，通过一定的策略在多个设备之间完成参数的同步和更新。这样一来，每个设备的计算量并不大，同时多个设备之间不存在数据依赖，是并行计算的，使得总的时间开销呈近似线性减小。</p>

<p>神经网络中的几乎所有op（除了batch normalization）一次只对一个样本做运算，不存在对多个样本的处理（即使是对一整个batch的数据做处理，数据之间也可以看做是并行处理），因此可以把一个minibatch中的样本分为几部分，指定给多个设备来处理。神经网络的这一特点很适合用数据并行来加速前向计算和后向计算过程。</p>

<p>但在权重更新阶段，需要把多个设备的结果平均起来得到整个minibatch的梯度，这会引入多个设备之间的数据传输，通信开销可能成为瓶颈。例如，文章<strong>[18]</strong>提到，在VGG-16网络上，数据并行的传输开销占到了总时间的85%。因此，近几年各大会议上关于分布式机器学习的文章，主要是集中在<strong>优化数据并行的传输和同步开销</strong>，通过设计更加合理的<strong>调度</strong>方案、<strong>压缩</strong>传输的数据量等方式优化数据传输时间。</p>

<p>同时，数据并行还要求在每个计算节点都保存网络的参数，这意味着网络参数会被复制为很多份，存储开销也比较大。除此之外，<strong>文章[19]</strong>指出，数据并行不能处理样本较大的数据集。因为数据并行不存在对网络本身的划分，每个样本都要完整地存储进GPU或其他计算设备的内存中，这使得对于样本较大的数据集，如果采用数据并行，就不能使用很大的batch size。</p>

<h3 id="中心化去中心化">中心化/去中心化</h3>

<p>数据并行的架构，主要分为两大类：中心化和去中心化。</p>

<p>中心化的网络结构往往会包括一个参数服务器（Parameter Server）<strong>（20(a)(c)）</strong>，参数服务器可能由一个或多个节点组成，由参数服务器担任所有计算节点的中介，统一收集所有计算节点的梯度值并计算参数更新，再发送给所有计算节点。去中心化的网络结构<strong>（20(b)(d)）</strong>则依赖于all-reduce操作在节点之间交换参数更新。</p>

<p><strong>文章[20]</strong>指出，设计中心化还是去中心化的DNN训练的网络结构，取决于很多因素，包括：网络拓扑、带宽、延迟、参数更新频率、容错机制等等。</p>

<p><img src="/images/DistributedML/004.png" align="center" /></p>

<ul>
  <li>
    <p><strong>中心化</strong></p>

    <p>最早提出参数服务器这个概念的文章是<strong>[21]</strong>，随后，最为著名的是李沐的文章<strong>[22]</strong>，他们提出了PS架构的第三代开源实现，针对机器学习应用的特点进行了再设计，使得针对机器学习的分布式应用的构建更为简洁。他们实现的PS架构高效、灵活并且robust。</p>

    <p><img src="/images/DistributedML/005.png" align="center" /></p>

    <p>参数服务器是一个抽象的概念，并不一定是一台服务器。分片化参数服务器Sharded PS<strong>[23] [24]（图21(a)）</strong>把权重参数分布到多个节点上存储，这样做减轻了参数服务器的拥挤程度，每个训练节点从相应的分片上获取并更新对应的权重参数。另一种层级化的PS架构<strong>[25] [26]（图21(b)）</strong>进一步缓解了资源的冲突。</p>

    <p>参数服务器架构不仅对性能有好处，也有益于容错机制的实现。机器学习中最简单的容错方案是checkpoint/restart，周期性地同步并把参数保存到非易失性存储，这很容易用PS架构来实现。DistBelief<strong>[23] [27]</strong>也对容错进行了更深入的研究，通过引入冗余计算、复制参数服务器提高了训练的弹性。参数服务器不仅可以减少本地存储开销，Project Adam<strong>[24]</strong>表明通过在参数服务器执行一些计算，可以使得节点之间的数据传输量更少。参数服务器也更容易处理异构的训练节点和网络设置。</p>
  </li>
  <li>
    <p><strong>去中心化</strong></p>

    <p>在去中心化的架构中，通过使用异步训练，更容易实现负载平衡。在非一致性、去中心化的参数更新时，一种方案是使用一个固定的交流图<strong>[28]</strong>，实现基于邻居的参数/梯度交换。另一种方案是使用Gossip算法，每个节点会跟固定数量的随机节点进行数据交换。</p>
  </li>
</ul>

<h3 id="模型一致性">模型一致性</h3>

<p>另一种改善数据传输开销的方式是异步地进行参数更新。同步更新得到的是一致性模型，异步更新得到非一致性模型。同步更新的收敛性更好，但同步更新往往需要等待最慢的节点，可能引入大量的同步开销，而异步更新虽然会引入一些误差，但总的时间开销更短。文章<strong>[29]</strong>详细分析了stragglers对同步更新的影响，以及gradient staleness对异步更新的影响。</p>

<p><img src="/images/DistributedML/006.png" align="center" /></p>

<p>一致性模型（同步的）：每一个计算节点都能拿到最新的权重参数（图20(a)(b)）。即所有的节点在开始计算下一个minibatch之前都要互相交换他们得到的updates（交换的过程可以是中心化的，也可以是去中心化的）。</p>

<p>非一致性模型（异步的）：每一个计算节点所拥有的权重不一定是最新的(图20(c))。对于计算节点$i$，在时间t所拥有的权重参数记为$w^{(\tau, i)}$ ，$\tau \leq t$， $(t-\tau)$被称为staleness（or lag）。异步更新的SGD最早的文章是<strong>[30]</strong>。比较著名的非一致性SGD算法是HOGWILD共享内存算法<strong>[31]</strong>，文章证明了HOGWILD对稀疏的学习问题是收敛的，其他<strong>文章[32] [33]</strong>也证明了HOGWILD对凸优化和非凸优化问题是收敛的。HOGWILD最初是为shared-memory系统设计的，后来也被延伸到distributed-memory系统中使用<strong>[23] [34]</strong>。</p>

<p>为了在异步更新时保证正确性，Stale-Synchronous Parallelism (SSP)<strong>[35]</strong>在一致性模型和非一致性模型之间提出了一个折中方案(图20(d))。当一个计算节点达到了最大的staleness，强制性地执行一次全局同步，从而将staleness限定在一定的界限内。文章<strong>[29]</strong>提出了另一种折中方案，通过引入backup workers（直接丢弃最慢的一部分worker的结果），缓解了同步更新的同步开销，同时避免了引入异步更新的staleness。</p>

<h3 id="其他">其他</h3>

<p>数据并行的一个重要的参数就是minibatch size。<strong>文章[36]</strong>对batch size对训练时间的影响给出了比较深入的分析。主要规律是，增加batch size最初会减少训练的步数，但收益会逐渐减小，直到增加batch size再也不会减少训练步数为止。</p>

<p>限制了batch size的一个瓶颈就是Batch Normalization op，因此有些工作针对数据并行的特点对BN op进行了重新的设计，从而增强数据并行的可扩展性。最近的工作把minibatch size增加到了8k <strong>[37]</strong>，32k <strong>[38]</strong>，甚至64k <strong>[39]</strong>。</p>

<p>有很多粗粒度的数据并行工作是基于MapReduce来设计的。例如ParallelSGD <strong>[40]</strong>、<strong>文章[41]</strong>、<strong>文章[42]</strong>、<strong>文章[43]</strong>。使用MapReduce很容易把并行的任务调度到不同的计算节点上，但很难进行DNN-specific的优化，因此很多工作使用MPI来设计细粒度的数据并行。例如，通过异步执行和流水线来减少延迟<strong>[44]</strong>、把mini-batch拆分为micro-batch<strong>[45] [46]</strong>。</p>

<h2 id="模型并行">模型并行</h2>

<p>模型并行相对数据并行来说，更加复杂，也更难被实际应用。其核心思想是，将一个神经网络切分成多个部分，每个部分单独部署到某个计算设备上，每个计算设备只负责它被分配到的那一部分网络的训练，由多个计算设备共同完成对一个神经网络的训练过程。</p>

<p>模型并行相对于数据并行的优点是，能处理规模更大的网络。某些规模较大的神经网络不能被单个计算设备容纳，因此无法采用数据并行的方案，却可以采用模型并行。同时，模型并行使得神经网络的不同部分在不同的计算节点上被计算，这可以节省内存开销（每个计算节点不需要保存整个网络），但由于神经网络层与层之间存在数据依赖，因此模型并行会引入额外的数据传输开销。</p>

<p><img src="/images/DistributedML/012.jpg" align="center" /></p>

<p>模型并行可以根据划分的粒度分为两大类，一类是<strong>按照layer粒度来划分</strong>（图(a)），另一类是<strong>按照neuron （神经元）粒度来划分</strong>（图(b)）。神经网络一般由很多_op_组成，每个_op_可以理解为由很多_neuron_组成的_layer_，一个_op_接收一个或多个_Tensor_进行计算后输出一个_Tensor_，这个过程其实是由该_layer_中的每个_neuron_对输出_Tensor_的相应的值进行计算，合并起来组成输出的Tensor。第一类模型并行的方案就是把每个_layer_作为一个整体部署到不同的设备上，也就是<strong>_op_划分</strong>，组成该<em>layer</em>的所有<em>neuron</em>就会被统一划分到相同设备上，这是比较粗粒度的划分方案。第二类方案就是把每一个_layer_划分为几部分，即把组成该<em>layer</em>的<em>neuron</em>分为几部分部署到不同的设备上，也就是<strong><em>neuron</em>划分</strong>，每个设备上的不同部分针对输入该_layer_的_Tensor_的不同部分进行计算，是比较细粒度的划分方案。</p>

<h3 id="op划分">op划分</h3>

<p>按照op粒度寻找最优的分割方式的问题，也被称为“<strong>op放置问题</strong>”<strong>（device placement problem</strong>）。</p>

<p>因为神经网络是一种前后计算单元存在严重数据依赖的计算模型，因此当我们把神经网络按照op的粒度切割成几部分部署到不同的计算设备上时，每一个计算设备都要等待上一个计算设备完成计算后，将神经网络的中间结果传输过来，才能开始计算。这个过程其实不是并行执行的，而是串行的，实现了<strong>“分布式计算”</strong>而并没有实现<strong>“并行计算”</strong>。因此，可以认为，op划分解决了“单个设备装不下整个网络”的问题，却没有对神经网络的训练过程起到加速的作用。不过，在某些存在并行结构的网络上，<strong>op划分</strong>理论上确实可以实现<strong>“并行加速”</strong>。例如在Inception网络中，存在大量的多分支网络块，每个分支之间不存在数据依赖，对于这样的结构，如果将多个分支部署到不同的计算设备上，理论上可以实现训练过程的加速，但在实际部署进行训练时，需要考虑数据传输开销，不一定能实现训练的加速。</p>

<p>关于<strong>op划分</strong> 的研究并不是很多，可能的原因是单个设备“装不下”的神经网络并不多，存在“并行结构”的网络也不多，导致该并行方案的适用场景较少。</p>

<p>在已有的研究中，最著名的是Google提出的<em>基于强化学习的op放置问题优化方案</em><strong>[47]</strong>，他们使用一个强化学习模型来寻找最优的划分方案，在这之后他们进一步优化了自己的工作，在对神经网络进行划分之前，先用另一个强化学习模型对op进行分组，分组后，大大减少了强化学习模型需要处理的op数量，从而得到了一个<em>分层的强化学习模型</em><strong>[48]</strong>。他们的工作在RNNLM、NMT和Inception-V3三种网络上得到了比人工专家更好的划分方案，但观察他们的结果可以发现，在某些情况下，多个GPU并不能带来性能提升，强化学习模型最终给出的方案仍是把所有op全部放在同一个设备上。</p>

<p>基于Google的工作，有另一个研究团队进一步做了更多的工作。他们首先提出了<em>Spotlight</em><strong>[49]</strong>，优化了Google的强化学习算法，使用_Proximal Policy_，而不是_Gradient Policy_。同时，他们还对“op放置问题”做了理论上的进一步抽象，提炼为一个<strong>马尔科夫决策过程</strong>。他们进一步提出了<em>Post</em><strong>[50]</strong>，结合了_Proximal Policy_和_Cross-entropy Minimization_（一种采样策略），最终他们的方法得到的部署方案能实现比Google的部署方案短63.7%的训练时间。</p>

<p>不过以上工作有个问题就是，没有神经网络的性能预测模型。强化学习模型每次训练需要用到目标神经网络的运行时间作为奖励值，他们通过真实部署并运行实际的模型来得到运行时间，这部分时间开销是十分巨大的。</p>

<h3 id="neuron划分">neuron划分</h3>

<p>从neuron的角度进行划分，也可看做是对Tensor进行划分。把神经网络每一层的不同神经元划分到不同的计算节点，也就是把一个4d的tensor在Channel、Height、Width维度上进行划分（数据并行是划分Sample维度）。</p>

<p>相比op划分的优点是，可以使得多个设备之间的负载更加均衡。代表性的文章是Eurosys’19的<em>Tofu</em><strong>[51]</strong>，抽象出了Tensor划分的模式：<em>partition-n-reduce</em>。_partition-n-reduce_的意思是，神经网络中的op $c$ 可以被并行到多个设备上执行，每个设备上执行的是一样的$c$，但仅对输入数据的一部分进行计算，最终的输出tensor $O$ 可以由两种方式得到：1)$O$是每个设备上的op $c$ 输出tensor在某个维度上的拼接；2)$O$是每个设备上的op $c$输出的tensor的element-wise的reduction。</p>

<p>斯坦福团队的文章<strong>[52]</strong>对于单个神经网络层从_height/width/channel_等维度进行划分。Sysml’19的文章<em>Beyond</em><strong>[53]</strong>是斯坦福团队进一步的工作，他们提出了一种 _“超越数据并行和模型并行”_的方案，其核心思想是，提出了一个更大的搜索/模型划分空间，以前的数据并行是从_Sample_的维度进行划分，模型并行是从_Parameter_的维度进行划分，现在他们的框架不仅能从_Sample_和_Parameter_的维度进行划分，还能从_Operator_和_Attribute_的维度进行划分，于是构成了一个被称作_SOAP_的搜索空间。在我看来，其实SOAP搜索空间里的Operator和Parameter仍是模型并行的范畴，Attribute确实跟模型并行不太一样，以卷积op为例，Attribute指的是输入数据内部的某些维度，比如图片的width和height，在这两个维度上对数据进行划分后，不同的设备是在输入图片的一部分区域进行卷积，这时，每个设备上仍然有当前op的全部参数（根据卷积的定义，即使只计算一部分区域，仍然需要全部的卷积核参数），因此，模型并未被拆分，拆分的是数据。而如果从输入图片的channel维度进行划分，情况就有所不同，因为对于不同channel的卷积需要的是不同的卷积核，这时，不同的设备被分配到了所有卷积核参数的一部分，此时属于模型并行。</p>

<p>除此之外，他们还提出了一个用来对神经网络运行时间进行模拟的simulator。这个simulator不对单个op的运行时间进行预测，而是直接profiling得到每个op的运行时间。对于整个神经网络，这个simulator将其抽象为一个_任务图_，从而能够比较清晰地知道不同任务（此处的任务已经是被划分后的op）的起止时间，是否冲突，从而能用一些搜索算法找到任务分布式部署的最优解。他们的simulator的预测时间误差在30%以内。</p>

<p>斯坦福的工作与Tofu其实有很多相同点。虽然两篇文章的表述不同，但其实都是neuron粒度的划分方案。FlexFlow的Parameter和Attribute维度在Tofu的文中都有体现。</p>

<p>几篇文章的不同点是，Tofu是基于现有的深度学习框架MXNet设计的，而FlexFlow是自己设计实现了新的框架（基于Legion）。同时，Tofu提出了一种新的描述性语言TDL(Tensor Description Language)，通过框架对每个tensor的分析得出划分的并行度，而FlexFlow要求用户人为地指定并行度。总的来说，Tofu比FlexFlow更加地通用。</p>

<p>对神经网络进行neuron粒度的划分，会在划分边界产生halo exchange的问题，即边界神经元需要用到邻近神经元的信息，从而产生了数据交换。<strong>文章[19]</strong>也从height、width维度实现了neuron划分，并且通过overlap halo exchange以及计算过程，实现了加速。另外，<strong>文章[54]</strong>提出了Tiled and Locally-Connected Networks，通过把卷积核分解来缓解halo exchanges的问题。LCN不存在权重共享，但权重共享是CNN一大特点，因此LCN并不常用。</p>

<h2 id="混合并行结合数据并行和模型并行">混合并行（结合数据并行和模型并行）</h2>

<p>在很多时候，数据并行和模型并行并不是对立的，而是要结合起来使用的。对多种并行方式的混合使用，可以克服其中每种方式的不足。</p>

<p>文章<strong>[55]</strong>提出的一种思路是，按照神经网络的不同层来采用不同的并行方案，例如，存在密集连接的具有大量参数的层，如全连接层，更倾向于使用模型并行来减少参数同步的开销；而卷积层和池化层更倾向于使用数据并行来排除来自前一层的数据传输。<strong>文章[56]</strong>也适用了类似的方案，并从理论上分析了communication cost。</p>

<p>另一个例子是AMPNet<strong>[44]</strong>。AMPNet是CPU上的DNN训练的异步实现。AMPNet使用了中间表示来实现细粒度的模型并行。层内和层间的并行任务被异步调度。并且，动态控制流的异步执行使得前向计算、后向更新、权重更新可以被流水化。</p>

<p>几个采用了混合并行来构建的大规模深度学习框架是：</p>

<p>DistBelief<strong>[23]</strong>。DistBelief分布式系统结合了三类并行策略。训练过程是在模型的多个复制上同时进行的，每个复制采用不同的数据进行训练（数据并行）。在每个复制中，模型同一层的不同神经元是分布式部署的（模型并行），不同层之间也是并行的（流水线并行）。</p>

<p>Project Adam<strong>[24]</strong>也采用了类似的思想。先是对整个网络垂直切分，即按照neuron粒度把每个layer划分到不同的设备上，因为这会最小化机器间的数据传输。在每个机器内部通过多线程实现了数据并行，每个线程计算不同的图片，异步且无锁地更新权重。</p>

<h2 id="流水线并行">流水线并行</h2>

<p>在前文_<strong>op划分</strong>_部分，我们讲到，如果对神经网络进行op划分，其实这并没有使得整个神经网络被“并行计算”，因为前后的op存在数据依赖。这个现象很像计算机中指令的执行过程，在5级流水CPU中，一条指令被分为5个阶段来执行，前后阶段存在数据依赖，在不引入流水线时，很多的部件会有大量的闲置和等待时间，一旦引入流水线，不仅部件的利用率增大了，而且总的执行时间也减少了。</p>

<p>数据并行、模型并行都可以看做是对一个batch内的数据并行计算（intra-batch parallelization）。而流水线并行可以看做是在batch之间实现了并行（inter-batch parallelization）。</p>

<p>Google提出了GPipe<strong>[57]</strong>，将每次训练的mini-batch拆分为多个micro-batch，流水线的每个阶段计算一个micro-batch。在多个micro-batch完成后进行同步，相当于每个mini-batch同步一次，是同步更新。不存在异步更新造成的参数污染问题。</p>

<p>PipeDream<strong>[58]</strong>则提出了流水线的另一种实现。PipeDream使用异步更新，能保持硬件资源最大化利用，流水线效率更高。但为了保证神经网络计算的正确性，同一个mini-batch的前向计算和后向计算过程必须使用同一个版本参数。因此，PipeDream会在每个节点存储多个版本的参数。同时，由于流水线各个阶段的运行时间越接近，整个流水线的并行效率就越高，PipeDream提出了一套自动划分机制，通过一个快速的profiling过程来决定如何划分op。由于某些op跟其他op的运行时间相差实在太大，PipeDream也会在某些op使用数据并行，来使得整个网络各个layer的时间更加均衡。</p>

<h2 id="最优部署策略的搜索问题">最优部署策略的搜索问题</h2>

<p>为了寻找最优的分布式划分/部署方案，往往存在一个巨大的搜索空间，如何高效地找到最优的解，不同的文章采用了不同的方法。例如，<strong>[47]</strong>采用了强化学习。<strong>[19] **使用了图的最短路径算法、</strong>[52]<strong>使用了贪心算法、</strong>[53]<strong>使用了马尔科夫-蒙特卡洛搜索算法、</strong>[51]**使用了动态规划算法。</p>

<ul>
  <li>
    <p><strong>动态规划</strong></p>

    <p>文章<strong>[13]</strong>存在的搜索问题是，在分布式的环境中，如何找到最优的系统配置参数，使得单个epoch的训练时间最短，他们的问题空间既包含数据并行，也包含模型并行。</p>

    <p>简单地说，他们设计的策略就是用贪心算法寻找work-resource 的mapping（把神经网络的哪部分放到哪个设备上），用动态规划算法寻找对网络的切分。既然是动态规划，就有DP-equation：</p>

    <p><img src="/images/DistributedML/019.png" width="400" align="center" /></p>

    <p>$T_{epoch} ([1,l],p_l)$  是从layer 1 到 layer $l$ 的总时间，layer $l$ 有 $p_l$ 个partition。$U(l, p_l)$ 是layer $l$ 的计算时间。$M(l, l-1, p_l, p_{l-1})$ 是layer $l-1$ 和 layer $l$ 之间的数据传输时间。</p>
  </li>
  <li>
    <p><strong>强化学习</strong></p>

    <p>文章<strong>[47]</strong>面临的问题是，对于分布式环境中的一个神经网络，把每个op放到哪个设备上能带来最好的性能。这篇文章采用了强化学习。强化学习只是一个学习策略，而他们要训练的模型是一个LSTM循环神经网络。他们这个循环神经网络每次读入一个序列的op信息，输出每个op放在哪个设备的结果。然后真实地按照LSTM输出的结果部署整个网络并运行，测得运行时间，用这个运行时间作为强化学习的reward，来对LSTM进行训练。</p>

    <p>文章<strong>[49]</strong>进一步优化了强化学习的算法，使用了_Proximal Policy_，而不是_Gradient Policy_，得到了更好的效果。</p>
  </li>
  <li>
    <p><strong>马尔科夫-蒙特卡洛</strong></p>

    <p>文章<strong>[53]</strong>面临的搜索问题是，对于给定的operator graph和device toppology，找到SOAP搜索空间中最高效的并行策略。可以通过归约到<em>minimum makespan</em>问题，证明这是NP-hard的问题。这篇文章使用了Metropolis-Hastings算法，简单地说就是每一步基于目前的并行策略进行一定的随机化修改，得到新的策略，利用Performance Model对新的并行策略进行时间的预测，根据新旧策略的时间预测值，按照一定的概率接受新的并行策略。终止条件是，搜索时间budget耗尽，或者有一半的搜索时间都没有得到新的改进。</p>
  </li>
  <li>
    <p><strong>贪心算法</strong></p>

    <p>文章<strong>[52]</strong>面临的搜索问题是，对于一个神经网络想要构造layer-wise的并行策略，即在每个layer采用不同的并行策略，最终使得整个网络时间最短。他们采用的做法是，设计了一个图归约算法，通过节点删除、边删除两个策略将完整的神经网络归约为简化的神经网络，再对简化网络中每一层可能的并行策略进行枚举，找到最优的策略后，再进行图归约的逆操作，寻找每个节点的最优并行策略。他们通过证明，表示这样的过程找到的layer-wise策略是最优的。这个算法本质上就是贪心算法。</p>
  </li>
  <li>
    <p><strong>图最短路径算法</strong></p>

    <p>文章<strong>[19]</strong>面临的搜索问题是，想要寻找一个layer-wise的并行策略（与上一小节类似），他们采取的方法是，首先对于每个layer按照一定的Heuristic给出可能的并行策略，然后构造一个图，从layer $i$ 的每种策略生成一条边到child layer $j$ 的每一种策略，边的权重就是layer $i$ 该并行策略按照Performance model 给出的时间预测值。然后对这个构造出的图，寻找从第一个layer到最后一个layer的最短路径。由于是有向无环图，所以可以在线性时间内被找到。同时，如果对于某些layer有大量的候选策略，可以设计一定的剪枝策略进行优化。这样的算法似乎本质上也是贪心算法。</p>
  </li>
</ul>

<h2 id="针对数据传输的优化">针对数据传输的优化</h2>

<p>前面已经提到，在分布式深度学习训练的过程中，数据传输的开销往往是限制了不能大规模扩展的瓶颈。有很多工作已经被提出，用于对数据传输进行优化。主要分为两大类：（1）减少消息的大小和数量；（2）优化消息的传输顺序，Overlap消息传输过程和计算过程。</p>

<p>第一类，又可分为两个分支：量化和稀疏化。第二类，主要是设计communication scheduler。</p>

<h3 id="量化-quantization">量化 Quantization</h3>

<p>量化就是把连续的信息映射到表示一组（或一个范围的）值的buckets中。<strong>文章[59]</strong>指出，参数和梯度的值的分布范围往往较窄，因此量化方法可以比较有效地减少每个参数的表示位数。</p>

<p>量化可以用于训练过程<strong>[60] [61] [62]</strong>，也可以用于推理过程<strong>[63] [64]</strong>，把训练后的参数量化表示。</p>

<p>量化通常是通过减少浮点数的位数来实现的<strong>[32] [60] [61]</strong>，例如把32位浮点数转化为16位浮点数（半精度）。减少位数的方式并不能直接被应用，往往还需要对参数进行四舍五入，<strong>[61]</strong>提出了随机四舍五入方法，使得参数值满足正确的期望值。<strong>[65]</strong>使用了Huffman编码在不降低收敛性的前提下提高存储效率。此外，还有一些工作将将网络量化为二进制参数、三进制参数、三进制梯度、二进制参数+三进制梯度等等形式。</p>

<h3 id="稀疏化-sparsification">稀疏化 Sparsification</h3>

<p>DNN（尤其是CNN）在参数更新时的梯度是比较稀疏的，并不是每个参数都需要更新。</p>

<p>第一个利用稀疏化梯度的应用是<strong>[66]</strong>，设置一个固定的threshold，低于threshold的值不会被送出。之后的一些工作提出了相对的以及适应性的threshold，目的都是只传输“重要”的值。</p>

<p>在中心化的网络架构中，稀疏化很容易实现，稀疏化的消息直接在PS和agents之间交换，而去中心化的网络架构中，每个节点可能得到梯度不同维度的更新，Kylix<strong>[67]</strong>用两个步骤实现了稀疏化的allreduce：第一步交换indices，第二步交换数据。SparCML<strong>[68]</strong>则支持索引值任意改变的allreduce操作。</p>

<h3 id="调度优化-communication-scheduler">调度优化 Communication Scheduler</h3>

<p><img src="/images/DistributedML/014.png" width="450" align="center" /></p>

<p><strong>P3（Priority-based Parameter Propagation）[69]</strong>提出，在一般的神经网络训练过程中，是按层进行参数的更新，而且在后向计算过程中最先被计算完成的layer的参数，在下一个迭代过程中，最后被使用。因此，把参数被使用的先后顺序纳入考虑范围，并且对参数进行分片，可以使带宽更高效地被利用，因此该篇文章为参数引入了更新优先级，并在MXNet上进行了实现，得到了更好的性能。</p>

<p><strong>TicTac[70]</strong>是一个类似的工作，也是对参数进行有优先级的更新。不过这篇文章给出了两种heuristics，TIC和TAC。TIC是Timing-Independent Communication scheduling，TAC是Timing-Aware Communication scheduling。区别在于赋予优先级时，是否考虑op运行时间的因素。</p>

<p>字节跳动团队的<strong>ByteScheduler[71]</strong>，是结合了以上两篇文章的特点，结合了priority-based communication scheduling以及tensor partitioning，推出的通用性communication scheduler。其通用性体现在，不仅支持多种机器学习框架，也支持多种数据并行架构，例如PS、all-reduce。他们之所以能实现通用性，是因为他们引入了一个统一的中间表示层Dependency Proxy，有了这个中间表示，他们就不需要去修改各个机器学习框架的源码，而是直接以插件的形式把scheduler引入相应的框架之中。</p>

<h3 id="其他方法">其他方法</h3>

<p>Project Adam通过发送activations和errors而不是参数本身，来减小全连接层的内存占用。Petuum框架<strong>[72] [73] [74]</strong>进一步延伸这个方法，传输分解后的外积，将这个思想一般化为Sufficient Factor Broadcasting (SFB) <strong>[75]</strong></p>

<p>另一种减少DNN内存占用的方法是，设计特殊的DNN结构。例如，构造1x1卷积层<strong>[76]</strong>，对卷积的tensor进行reshaping<strong>[77]</strong>或采用Tucker Decomposition<strong>[78]</strong>、可分解的卷积<strong>[79] [80]</strong>。</p>

<h2 id="参考文献">参考文献</h2>

<p>[1] Robbins, H., &amp; Monro, S. (1951). <strong>A stochastic approximation method.</strong> <em>The annals of mathematical statistics</em>, 400-407.<br />
[2] Oyama, Y., Nomura, A., Sato, I., Nishimura, H., Tamatsu, Y., &amp; Matsuoka, S. (2016, December). <strong>Predicting statistics of asynchronous SGD parameters for a large-scale distributed deep learning system on GPU supercomputers.</strong> In <em>2016 IEEE International Conference on Big Data (Big Data)</em> (pp. 66-75). IEEE.<br />
[3] Demmel, J., &amp; Dinh, G. (2018). <strong>Communication-optimal convolutional neural nets.</strong> <em>arXiv preprint arXiv:1802.06905</em>.<br />
[4] Cong, J., &amp; Xiao, B. (2014, September). <strong>Minimizing computation in convolutional neural networks.</strong> In <em>International conference on artificial neural networks</em> (pp. 281-290). Springer, Cham.<br />
[5] Vasilache, N., Johnson, J., Mathieu, M., Chintala, S., Piantino, S., &amp; LeCun, Y. (2014). <strong>Fast convolutional nets with fbfft: A GPU performance evaluation.</strong> <em>arXiv preprint arXiv:1412.7580</em>.<br />
[6] Lavin, A., &amp; Gray, S. (2016). <strong>Fast algorithms for convolutional neural networks.</strong> In <em>Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition</em> (pp. 4013-4021).<br />
[7] Chetlur, S., Woolley, C., Vandermersch, P., Cohen, J., Tran, J., Catanzaro, B., &amp; Shelhamer, E. (2014). <strong>cudnn: Efficient primitives for deep learning.</strong> <em>arXiv preprint arXiv:1410.0759</em>.<br />
[8] Li, C., Yang, Y., Feng, M., Chakradhar, S., &amp; Zhou, H. (2016, November). <strong>Optimizing memory efficiency for deep convolutional neural networks on GPUs.</strong> In <em>SC’16: Proceedings of the International Conference for High Performance Computing, Networking, Storage and Analysis</em>(pp. 633-644). IEEE.<br />
[9] Appleyard, J., Kocisky, T., &amp; Blunsom, P. (2016). <strong>Optimizing performance of recurrent neural networks on gpus.</strong> <em>arXiv preprint arXiv:1604.01946</em>.<br />
[10] Gruslys, A., Munos, R., Danihelka, I., Lanctot, M., &amp; Graves, A. (2016). <strong>Memory-efficient backpropagation through time.</strong> In <em>Advances in Neural Information Processing Systems</em> (pp. 4125-4133).<br />
[11] Chen, T., Xu, B., Zhang, C., &amp; Guestrin, C. (2016). <strong>Training deep nets with sublinear memory cost.</strong> <em>arXiv preprint arXiv:1604.06174</em>.<br />
[12] Diamos, G., Sengupta, S., Catanzaro, B., Chrzanowski, M., Coates, A., Elsen, E., … &amp; Satheesh, S. (2016, June). <strong>Persistent rnns: Stashing recurrent weights on-chip.</strong> In <em>International Conference on Machine Learning</em> (pp. 2024-2033).<br />
[13] Yan, F., Ruwase, O., He, Y., &amp; Chilimbi, T. (2015, August). <strong>Performance modeling and scalability optimization of distributed deep learning systems.</strong> In <em>Proceedings of the 21th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining</em> (pp. 1355-1364).<br />
[14] Qi, H., Sparks, E. R., &amp; Talwalkar, A. (2016). <strong>Paleo: A performance model for deep neural networks.</strong><br />
[15] Cai, E., Juan, D. C., Stamoulis, D., &amp; Marculescu, D. (2017). <strong>Neuralpower: Predict and deploy energy-efficient convolutional neural networks.</strong> <em>arXiv preprint arXiv:1710.05420</em>.<br />
[16] Justus, D., Brennan, J., Bonner, S., &amp; McGough, A. S. (2018, December). <strong>Predicting the computational cost of deep learning models.</strong> In <em>2018 IEEE International Conference on Big Data (Big Data)</em> (pp. 3873-3882). IEEE.<br />
[17] Song, M., Hu, Y., Chen, H., &amp; Li, T. (2017, February). <strong>Towards pervasive and user satisfactory CNN across GPU microarchitectures.</strong> In <em>2017 IEEE International Symposium on High Performance Computer Architecture (HPCA)</em> (pp. 1-12). IEEE.<br />
[18] Simonyan, K., &amp; Zisserman, A. (2014). <strong>Very deep convolutional networks for large-scale image recognition.</strong> <em>arXiv preprint arXiv:1409.1556</em>.<br />
[19] Dryden, N., Maruyama, N., Benson, T., Moon, T., Snir, M., &amp; Van Essen, B. (2019, May). <strong>Improving strong-scaling of CNN training by exploiting finer-grained parallelism.</strong> In <em>2019 IEEE International Parallel and Distributed Processing Symposium (IPDPS)</em> (pp. 210-220). IEEE.<br />
[20] Lian, X., Zhang, C., Zhang, H., Hsieh, C. J., Zhang, W., &amp; Liu, J. (2017). <strong>Can decentralized algorithms outperform centralized algorithms? a case study for decentralized parallel stochastic gradient descent.</strong> In <em>Advances in Neural Information Processing Systems</em> (pp. 5330-5340).<br />
[21] Smola, A., &amp; Narayanamurthy, S. (2010). <strong>An architecture for parallel topic models.</strong> <em>Proceedings of the VLDB Endowment</em>, <em>3</em>(1-2), 703-710.<br />
[22] Li, M., Andersen, D. G., Park, J. W., Smola, A. J., Ahmed, A., Josifovski, V., … &amp; Su, B. Y. (2014). <strong>Scaling distributed machine learning with the parameter server.</strong> In <em>11th {USENIX} Symposium on Operating Systems Design and Implementation ({OSDI} 14)</em> (pp. 583-598).<br />
[23] Dean, J., Corrado, G., Monga, R., Chen, K., Devin, M., Mao, M., … &amp; Le, Q. V. (2012). <strong>Large scale distributed deep networks.</strong> In <em>Advances in neural information processing systems</em> (pp. 1223-1231).<br />
[24] Chilimbi, T., Suzue, Y., Apacible, J., &amp; Kalyanaraman, K. (2014). <strong>Project adam: Building an efficient and scalable deep learning training system.</strong> In <em>11th {USENIX} Symposium on Operating Systems Design and Implementation ({OSDI} 14)</em>(pp. 571-582).<br />
[25] Gupta, S., Zhang, W., &amp; Wang, F. (2016, December). <strong>Model accuracy and runtime tradeoff in distributed deep learning: A systematic study</strong>. In <em>2016 IEEE 16th International Conference on Data Mining (ICDM)</em> (pp. 171-180). IEEE.<br />
[26] Yu, Y., Jiang, J., &amp; Chi, X. (2016, December). <strong>Using supercomputer to speed up neural network training.</strong> In <em>2016 IEEE 22nd International Conference on Parallel and Distributed Systems (ICPADS)</em> (pp. 942-947). IEEE.<br />
[27] Le, Q. V. (2013, May). <strong>Building high-level features using large scale unsupervised learning.</strong> In <em>2013 IEEE international conference on acoustics, speech and signal processing</em> (pp. 8595-8598). IEEE.<br />
[28] Lian, X., Zhang, W., Zhang, C., &amp; Liu, J. (2017). <strong>Asynchronous decentralized parallel stochastic gradient descent.</strong> <em>arXiv preprint arXiv:1710.06952</em>.<br />
[29] Chen, J., Pan, X., Monga, R., Bengio, S., &amp; Jozefowicz, R. (2016). <strong>Revisiting distributed synchronous SGD.</strong> <em>arXiv preprint arXiv:1604.00981</em>.<br />
[30] Tsitsiklis, J., Bertsekas, D., &amp; Athans, M. (1986). <strong>Distributed asynchronous deterministic and stochastic gradient optimization algorithms.</strong> <em>IEEE transactions on automatic control</em>, <em>31</em>(9), 803-812.<br />
[31] Recht, B., Re, C., Wright, S., &amp; Niu, F. (2011). <strong>Hogwild: A lock-free approach to parallelizing stochastic gradient descent.</strong> In <em>Advances in neural information processing systems</em> (pp. 693-701).<br />
[32] De Sa, C. M., Zhang, C., Olukotun, K., &amp; Ré, C. (2015). <strong>Taming the wild: A unified analysis of hogwild-style algorithms.</strong> In <em>Advances in neural information processing systems</em> (pp. 2674-2682).<br />
[33] Lian, X., Huang, Y., Li, Y., &amp; Liu, J. (2015). <strong>Asynchronous parallel stochastic gradient for nonconvex optimization.</strong> In <em>Advances in Neural Information Processing Systems</em> (pp. 2737-2745).<br />
[34] Noel, C., &amp; Osindero, S. (2014, December). <strong>Dogwild!-distributed hogwild for cpu &amp; gpu.</strong> In <em>NIPS Workshop on Distributed Machine Learning and Matrix Computations</em> (pp. 693-701).<br />
[35] Ho, Q., Cipar, J., Cui, H., Lee, S., Kim, J. K., Gibbons, P. B., … &amp; Xing, E. P. (2013). <strong>More effective distributed ml via a stale synchronous parallel parameter server.</strong> In <em>Advances in neural information processing systems</em> (pp. 1223-1231).<br />
[36] Shallue, C. J., Lee, J., Antognini, J., Sohl-Dickstein, J., Frostig, R., &amp; Dahl, G. E. (2018). <strong>Measuring the effects of data parallelism on neural network training.</strong> <em>arXiv preprint arXiv:1811.03600</em>.<br />
[37] Goyal, P., Dollár, P., Girshick, R., Noordhuis, P., Wesolowski, L., Kyrola, A., … &amp; He, K. (2017). <strong>Accurate, large minibatch sgd: Training imagenet in 1 hour.</strong> <em>arXiv preprint arXiv:1706.02677</em>.<br />
[38] You, Y., Gitman, I., &amp; Ginsburg, B. (2017). <strong>Large batch training of convolutional networks.</strong> <em>arXiv preprint arXiv:1708.03888</em>.<br />
[39] Smith, S. L., Kindermans, P. J., Ying, C., &amp; Le, Q. V. (2017). <strong>Don’t decay the learning rate, increase the batch size.</strong> <em>arXiv preprint arXiv:1711.00489</em>.<br />
[40] Zinkevich, M., Weimer, M., Li, L., &amp; Smola, A. J. (2010). <strong>Parallelized stochastic gradient descent.</strong> In <em>Advances in neural information processing systems</em> (pp. 2595-2603).<br />
[41] Jiang, J., Cui, B., Zhang, C., &amp; Yu, L. (2017, May). <strong>Heterogeneity-aware distributed parameter servers</strong>. In <em>Proceedings of the 2017 ACM International Conference on Management of Data</em> (pp. 463-478).<br />
[42] Le, Q. V., Ngiam, J., Coates, A., Lahiri, A., Prochnow, B., &amp; Ng, A. Y. (2011). <strong>On optimization methods for deep learning.</strong><br />
[43] Zhang, K., &amp; Chen, X. W. (2014). <strong>Large-scale deep belief nets with mapreduce.</strong> <em>IEEE Access</em>, <em>2</em>, 395-403.<br />
[44] Gaunt, A. L., Johnson, M. A., Riechert, M., Tarlow, D., Tomioka, R., Vytiniotis, D., &amp; Webster, S. (2017). <strong>AMPNet: Asynchronous model-parallel training for dynamic neural networks.</strong> <em>arXiv preprint arXiv:1705.09786</em>.<br />
[45] Oyama, Y., Ben-Nun, T., Hoefler, T., &amp; Matsuoka, S. (2018, September). <strong>Accelerating deep learning frameworks with micro-batches.</strong> In <em>2018 IEEE International Conference on Cluster Computing (CLUSTER)</em> (pp. 402-412). IEEE.<br />
[46] Zlateski, A., Lee, K., &amp; Seung, H. S. (2016, November). <strong>ZNNi: maximizing the inference throughput of 3D convolutional networks on CPUs and GPUs.</strong> In <em>SC’16: Proceedings of the International Conference for High Performance Computing, Networking, Storage and Analysis</em> (pp. 854-865). IEEE.<br />
[47] Mirhoseini, A., Pham, H., Le, Q. V., Steiner, B., Larsen, R., Zhou, Y., … &amp; Dean, J. (2017, August). <strong>Device placement optimization with reinforcement learning.</strong> In <em>Proceedings of the 34th International Conference on Machine Learning-Volume 70</em> (pp. 2430-2439). JMLR. org.<br />
[48] Mirhoseini, A., Goldie, A., Pham, H., Steiner, B., Le, Q. V., &amp; Dean, J. (2018). <strong>A hierarchical model for device placement.</strong><br />
[49] Gao, Y., Chen, L., &amp; Li, B. (2018, July). <strong>Spotlight: Optimizing device placement for training deep neural networks.</strong> In <em>International Conference on Machine Learning</em> (pp. 1676-1684).<br />
[50] Gao, Y., Chen, L., &amp; Li, B. (2018). <strong>Post: Device placement with cross-entropy minimization and proximal policy optimization.</strong> In <em>Advances in Neural Information Processing Systems</em> (pp. 9971-9980).<br />
[51] Wang, M., Huang, C. C., &amp; Li, J. (2019, March). <strong>Supporting very large models using automatic dataflow graph partitioning.</strong> In <em>Proceedings of the Fourteenth EuroSys Conference 2019</em>(pp. 1-17).<br />
[52] Jia, Z., Lin, S., Qi, C. R., &amp; Aiken, A. (2018). <strong>Exploring hidden dimensions in parallelizing convolutional neural networks.</strong> <em>arXiv preprint arXiv:1802.04924</em>.<br />
[53] Jia, Z., Zaharia, M., &amp; Aiken, A. (2018). <strong>Beyond data and model parallelism for deep neural networks.</strong> <em>arXiv preprint arXiv:1807.05358</em>.<br />
[54] Ngiam, J., Chen, Z., Chia, D., Koh, P. W., Le, Q. V., &amp; Ng, A. Y. (2010). <strong>Tiled convolutional neural networks.</strong> In <em>Advances in neural information processing systems</em> (pp. 1279-1287).<br />
[55] Krizhevsky, A. (2014). <strong>One weird trick for parallelizing convolutional neural networks.</strong> <em>arXiv preprint arXiv:1404.5997</em>.<br />
[56] Ben-Nun, T., Levy, E., Barak, A., &amp; Rubin, E. (2015, November). <strong>Memory access patterns: the missing piece of the multi-GPU puzzle.</strong> In <em>SC’15: Proceedings of the International Conference for High Performance Computing, Networking, Storage and Analysis</em> (pp. 1-12). IEEE.<br />
[57] Huang, Y., Cheng, Y., Bapna, A., Firat, O., Chen, D., Chen, M., … &amp; Wu, Y. (2019). <strong>Gpipe: Efficient training of giant neural networks using pipeline parallelism.</strong> In <em>Advances in Neural Information Processing Systems</em> (pp. 103-112).<br />
[58] Narayanan, D., Harlap, A., Phanishayee, A., Seshadri, V., Devanur, N. R., Ganger, G. R., … &amp; Zaharia, M. (2019, October). <strong>PipeDream: generalized pipeline parallelism for DNN training.</strong> In <em>Proceedings of the 27th ACM Symposium on Operating Systems Principles</em> (pp. 1-15).<br />
[59] Köster, U., Webb, T., Wang, X., Nassar, M., Bansal, A. K., Constable, W., … &amp; Khosrowshahi, A. (2017). <strong>Flexpoint: An adaptive numerical format for efficient training of deep neural networks.</strong> In <em>Advances in neural information processing systems</em> (pp. 1742-1752).<br />
[60] Dettmers, T. (2015). <strong>8-bit approximations for parallelism in deep learning.</strong> <em>arXiv preprint arXiv:1511.04561</em>.<br />
[61] Gupta, S., Agrawal, A., Gopalakrishnan, K., &amp; Narayanan, P. (2015, June). <strong>Deep learning with limited numerical precision.</strong> In <em>International Conference on Machine Learning</em> (pp. 1737-1746).<br />
[62] Hubara, I., Courbariaux, M., Soudry, D., El-Yaniv, R., &amp; Bengio, Y. (2017). <strong>Quantized neural networks: Training neural networks with low precision weights and activations.</strong> <em>The Journal of Machine Learning Research</em>, <em>18</em>(1), 6869-6898.<br />
[63] Rastegari, M., Ordonez, V., Redmon, J., &amp; Farhadi, A. (2016, October). <strong>Xnor-net: Imagenet classification using binary convolutional neural networks.</strong> In <em>European conference on computer vision</em> (pp. 525-542). Springer, Cham.<br />
[64] Zhou, S., Wu, Y., Ni, Z., Zhou, X., Wen, H., &amp; Zou, Y. (2016). <strong>Dorefa-net: Training low bitwidth convolutional neural networks with low bitwidth gradients.</strong> <em>arXiv preprint arXiv:1606.06160</em>.<br />
[65] Han, S., Mao, H., &amp; Dally, W. J. (2015). <strong>Deep compression: Compressing deep neural networks with pruning, trained quantization and huffman coding.</strong> <em>arXiv preprint arXiv:1510.00149</em>.<br />
[66] Strom, N. (2015). <strong>Scalable distributed DNN training using commodity GPU cloud computing.</strong> In <em>Sixteenth Annual Conference of the International Speech Communication Association</em>.<br />
[67] Zhao, H., &amp; Canny, J. (2014, September). <strong>Kylix: A sparse allreduce for commodity clusters.</strong> In <em>2014 43rd International Conference on Parallel Processing</em> (pp. 273-282). IEEE.<br />
[68] Renggli, C., Ashkboos, S., Aghagolzadeh, M., Alistarh, D., &amp; Hoefler, T. (2019, November). <strong>Sparcml: High-performance sparse communication for machine learning.</strong> In <em>Proceedings of the International Conference for High Performance Computing, Networking, Storage and Analysis</em> (pp. 1-15).<br />
[69] Jayarajan, A., Wei, J., Gibson, G., Fedorova, A., &amp; Pekhimenko, G. (2019). <strong>Priority-based parameter propagation for distributed DNN training.</strong> <em>arXiv preprint arXiv:1905.03960</em>.<br />
[70] Hashemi, S. H., Jyothi, S. A., &amp; Campbell, R. H. (2018). <strong>TicTac: Accelerating distributed deep learning with communication scheduling.</strong> <em>arXiv preprint arXiv:1803.03288</em>.<br />
[71] Peng, Y., Zhu, Y., Chen, Y., Bao, Y., Yi, B., Lan, C., … &amp; Guo, C. (2019, October). <strong>A generic communication scheduler for distributed DNN training acceleration.</strong> In <em>Proceedings of the 27th ACM Symposium on Operating Systems Principles</em> (pp. 16-29).<br />
[72] Xing, E. P., Ho, Q., Dai, W., Kim, J. K., Wei, J., Lee, S., … &amp; Yu, Y. (2015). <strong>Petuum: A new platform for distributed machine learning on big data.</strong> <em>IEEE Transactions on Big Data</em>, <em>1</em>(2), 49-67.<br />
[73] Zhang, H., Hu, Z., Wei, J., Xie, P., Kim, G., Ho, Q., &amp; Xing, E. (2015). <strong>Poseidon: A system architecture for efficient gpu-based deep learning on multiple machines.</strong> <em>arXiv preprint arXiv:1512.06216</em>.<br />
[74] Zhang, H., Zheng, Z., Xu, S., Dai, W., Ho, Q., Liang, X., … &amp; Xing, E. P. (2017). <strong>Poseidon: An efficient communication architecture for distributed deep learning on {GPU} clusters.</strong> In <em>2017 {USENIX} Annual Technical Conference ({USENIX}{ATC} 17)</em> (pp. 181-193).<br />
[75] Xie, P., Kim, J. K., Zhou, Y., Ho, Q., Kumar, A., Yu, Y., &amp; Xing, E. P. (2016, June). <strong>Lighter-Communication Distributed Machine Learning via Sufficient Factor Broadcasting</strong>. In <em>UAI</em>.<br />
[76] Iandola, F. N., Han, S., Moskewicz, M. W., Ashraf, K., Dally, W. J., &amp; Keutzer, K. (2016). <strong>SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and&lt; 0.5 MB model size.</strong> <em>arXiv preprint arXiv:1602.07360</em>.<br />
[77] Li, D., Wang, X., &amp; Kong, D. (2018, April). <strong>Deeprebirth: Accelerating deep neural network execution on mobile devices.</strong> In <em>Thirty-second AAAI conference on artificial intelligence</em>.<br />
[78] Kim, Y. D., Park, E., Yoo, S., Choi, T., Yang, L., &amp; Shin, D. (2015). <strong>Compression of deep convolutional neural networks for fast and low power mobile applications.</strong> <em>arXiv preprint arXiv:1511.06530</em>.<br />
[79] Chollet, F. (2017). <strong>Xception: Deep learning with depthwise separable convolutions.</strong> In <em>Proceedings of the IEEE conference on computer vision and pattern recognition</em> (pp. 1251-1258).<br />
[80] Howard, A. G., Zhu, M., Chen, B., Kalenichenko, D., Wang, W., Weyand, T., … &amp; Adam, H. (2017). <strong>Mobilenets: Efficient convolutional neural networks for mobile vision applications.</strong> <em>arXiv preprint arXiv:1704.04861</em>.<br />
[81] Ben-Nun, T., &amp; Hoefler, T. (2019). <strong>Demystifying parallel and distributed deep learning: An in-depth concurrency analysis.</strong> <em>ACM Computing Surveys (CSUR)</em>, <em>52</em>(4), 1-43.</p>]]></content><author><name>Guodong Liu</name><email>me@lgd.gd</email></author><category term="Distributed System" /><category term="Deep Learning" /><summary type="html"><![CDATA[Intro 由于神经网络的规模越来越大，单个计算设备往往难以在短时间内完成对整个网络的训练，因此出现了大量的分布式神经网络训练方案，利用多个节点的计算能力加速训练过程。 而计算设备也在不断更新，可以是CPU、GPU以及TPU等更多类型的硬件。本文调研了分布式深度学习领域相关的最新文献，简要介绍该领域目前关注的主要问题和已有的解决方案。]]></summary></entry></feed>