<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>loosp</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://loosp.cn/</id>
  <link href="https://loosp.cn/" rel="alternate"/>
  <link href="https://loosp.cn/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, loosp</rights>
  <subtitle>And S is not for sayonara</subtitle>
  <title>loosp</title>
  <updated>2026-09-08T20:55:13.908Z</updated>
  <entry>
    <author>
      <name>loosp</name>
    </author>
    <category term="学习" scheme="https://loosp.cn/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="nccl" scheme="https://loosp.cn/tags/nccl/"/>
    <category term="allreduce" scheme="https://loosp.cn/tags/allreduce/"/>
    <category term="gpu" scheme="https://loosp.cn/tags/gpu/"/>
    <category term="分布式训练" scheme="https://loosp.cn/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%AD%E7%BB%83/"/>
    <category term="学习笔记" scheme="https://loosp.cn/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<h1 id="为什么说-AllReduce-ReduceScatter-AllGather？——结合-NCCL-Ring-AllReduce"><a href="#为什么说-AllReduce-ReduceScatter-AllGather？——结合-NCCL-Ring-AllReduce" class="headerlink" title="为什么说 AllReduce &#x3D; ReduceScatter + AllGather？——结合 NCCL Ring AllReduce"></a>为什么说 AllReduce &#x3D; ReduceScatter + AllGather？——结合 NCCL Ring AllReduce</h1><blockquote><p><strong>参考</strong></p><ul><li>NVIDIA NCCL Collective Operations：<a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html">https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html</a></li><li>Demystifying NCCL: An In-depth Analysis of GPU Communication Protocols and Algorithms, Fig. 4：<a href="https://arxiv.org/pdf/2507.04786">https://arxiv.org/pdf/2507.04786</a></li></ul></blockquote><p>其实直接看<a href="https://arxiv.org/pdf/2507.04786%E7%9A%84">https://arxiv.org/pdf/2507.04786的</a> fig.4就行了</p><p>NVIDIA NCCL 官方文档明确指出：</p><blockquote><p>ReduceScatter 后执行 AllGather，在结果上等价于 AllReduce。</p></blockquote><p>这个等式可以从两个层面理解：</p><ol><li><strong>语义上为什么等价</strong></li><li><strong>NCCL 的 Ring AllReduce 实际上怎样把这两个阶段做出来</strong></li></ol><h2 id="1-先从最终目标看"><a href="#1-先从最终目标看" class="headerlink" title="1. 先从最终目标看"></a>1. 先从最终目标看</h2><p>假设有 4 个 GPU，每个 GPU 上都有 4 个 chunk：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: [a0, a1, a2, a3]</span><br><span class="line">GPU1: [b0, b1, b2, b3]</span><br><span class="line">GPU2: [c0, c1, c2, c3]</span><br><span class="line">GPU3: [d0, d1, d2, d3]</span><br></pre></td></tr></table></figure><p>假设 reduction 操作是 SUM。</p><p>最终应该得到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">S0 = a0 + b0 + c0 + d0</span><br><span class="line">S1 = a1 + b1 + c1 + d1</span><br><span class="line">S2 = a2 + b2 + c2 + d2</span><br><span class="line">S3 = a3 + b3 + c3 + d3</span><br></pre></td></tr></table></figure><p>所以 AllReduce 的最终目标是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: [S0, S1, S2, S3]</span><br><span class="line">GPU1: [S0, S1, S2, S3]</span><br><span class="line">GPU2: [S0, S1, S2, S3]</span><br><span class="line">GPU3: [S0, S1, S2, S3]</span><br></pre></td></tr></table></figure><p>注意这里其实包含两个不同的问题：</p><p><strong>问题 1</strong>：怎样把 a0、b0、c0、d0 合成 S0，把 a1、b1、c1、d1 合成 S1，…… 这是 <strong>Reduce</strong>。</p><p>然后 <strong>问题 2</strong>：S0、S1、S2、S3 已经算出来以后，怎样让每一个 GPU 都拥有全部 S0~S3？这是 <strong>Gather &#x2F; Dissemination</strong>。</p><p>因此可以自然地把 AllReduce 拆成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">            AllReduce</span><br><span class="line"></span><br><span class="line">                │</span><br><span class="line">        ┌───────┴───────┐</span><br><span class="line">        │               │</span><br><span class="line">        ▼               ▼</span><br><span class="line"> ReduceScatter       AllGather</span><br><span class="line"></span><br><span class="line">把结果算出来       把结果传播给所有 GPU</span><br><span class="line">每个 GPU 留一块</span><br></pre></td></tr></table></figure><h2 id="2-ReduceScatter-做了什么？"><a href="#2-ReduceScatter-做了什么？" class="headerlink" title="2. ReduceScatter 做了什么？"></a>2. ReduceScatter 做了什么？</h2><p>ReduceScatter 并不是让每个 GPU 都立刻得到完整结果。</p><p>它做的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">所有 GPU 的输入</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">    Reduce</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">[S0, S1, S2, S3]</span><br><span class="line">       │</span><br><span class="line">       │ Scatter</span><br><span class="line">       ▼</span><br><span class="line"></span><br><span class="line">最后变成类似：</span><br><span class="line"></span><br><span class="line">GPU0: [S1]</span><br><span class="line">GPU1: [S2]</span><br><span class="line">GPU2: [S3]</span><br><span class="line">GPU3: [S0]</span><br></pre></td></tr></table></figure><p>具体哪个 rank 最后拥有哪个 chunk，取决于 Ring 的调度和 chunk 编号方式；关键不是编号，而是：</p><blockquote><p>ReduceScatter 完成后，每一个最终 reduced chunk 已经被完整计算出来，但它们分散在不同 GPU 上。</p></blockquote><p>也就是说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">完整逻辑结果：</span><br><span class="line"></span><br><span class="line">[S0, S1, S2, S3]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">物理分布：</span><br><span class="line"></span><br><span class="line">GPU0        GPU1        GPU2        GPU3</span><br><span class="line"> S1          S2          S3          S0</span><br></pre></td></tr></table></figure><p>此时缺的已经不是 reduction——所有的 S0~S3 都已经算好了，缺的是：</p><blockquote><p>“把这些 chunk 复制给其他 GPU。”</p></blockquote><p>这正好就是 AllGather。</p><h2 id="3-AllGather-接着做什么？"><a href="#3-AllGather-接着做什么？" class="headerlink" title="3. AllGather 接着做什么？"></a>3. AllGather 接着做什么？</h2><p>ReduceScatter 结束时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: S1</span><br><span class="line">GPU1: S2</span><br><span class="line">GPU2: S3</span><br><span class="line">GPU3: S0</span><br></pre></td></tr></table></figure><p>执行 AllGather 后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: [S0, S1, S2, S3]</span><br><span class="line">GPU1: [S0, S1, S2, S3]</span><br><span class="line">GPU2: [S0, S1, S2, S3]</span><br><span class="line">GPU3: [S0, S1, S2, S3]</span><br></pre></td></tr></table></figure><p>这已经和直接执行 AllReduce 的输出完全相同。</p><p>所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">ReduceScatter</span><br><span class="line">    │</span><br><span class="line">    │ 先算出所有 reduced chunk</span><br><span class="line">    │ 但每个 GPU 只保存其中一块</span><br><span class="line">    ▼</span><br><span class="line"></span><br><span class="line">GPU0: S1</span><br><span class="line">GPU1: S2</span><br><span class="line">GPU2: S3</span><br><span class="line">GPU3: S0</span><br><span class="line"></span><br><span class="line">    │</span><br><span class="line">    │ AllGather</span><br><span class="line">    ▼</span><br><span class="line"></span><br><span class="line">GPU0: S0 S1 S2 S3</span><br><span class="line">GPU1: S0 S1 S2 S3</span><br><span class="line">GPU2: S0 S1 S2 S3</span><br><span class="line">GPU3: S0 S1 S2 S3</span><br></pre></td></tr></table></figure><p>因此：</p><blockquote><p><strong>AllReduce &#x3D; ReduceScatter + AllGather</strong></p></blockquote><h2 id="4-Ring-AllReduce-是怎么实现这个过程的？"><a href="#4-Ring-AllReduce-是怎么实现这个过程的？" class="headerlink" title="4. Ring AllReduce 是怎么实现这个过程的？"></a>4. Ring AllReduce 是怎么实现这个过程的？</h2><p>论文 Fig. 4 给出了 NCCL 在 4 个 GPU 上执行 Ring AllReduce 的具体过程。</p><p>假设 Ring 为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 ──→ GPU1</span><br><span class="line"> ↑         │</span><br><span class="line"> │         ↓</span><br><span class="line">GPU3 ←── GPU2</span><br></pre></td></tr></table></figure><p>也就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GPU0 → GPU1 → GPU2 → GPU3 → GPU0</span><br></pre></td></tr></table></figure><p>每个 GPU：</p><ul><li>从左侧 &#x2F; predecessor 接收数据</li><li>向右侧 &#x2F; successor 发送数据</li></ul><p>整个 Ring AllReduce 可以看成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────────────┐</span><br><span class="line">│   ReduceScatter      │</span><br><span class="line">│                      │</span><br><span class="line">│ 数据一边绕 Ring      │</span><br><span class="line">│ 一边做 reduction     │</span><br><span class="line">└──────────┬───────────┘</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line">   每个 GPU 得到一个</span><br><span class="line">   完整 reduced chunk</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line">┌──────────────────────┐</span><br><span class="line">│      AllGather       │</span><br><span class="line">│                      │</span><br><span class="line">│ reduced chunk 继续   │</span><br><span class="line">│ 沿 Ring 传播         │</span><br><span class="line">└──────────┬───────────┘</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line"> 每个 GPU 得到全部 chunk</span><br></pre></td></tr></table></figure><h2 id="5-第一阶段：Ring-ReduceScatter"><a href="#5-第一阶段：Ring-ReduceScatter" class="headerlink" title="5. 第一阶段：Ring ReduceScatter"></a>5. 第一阶段：Ring ReduceScatter</h2><p>仍然使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: [a0, a1, a2, a3]</span><br><span class="line">GPU1: [b0, b1, b2, b3]</span><br><span class="line">GPU2: [c0, c1, c2, c3]</span><br><span class="line">GPU3: [d0, d1, d2, d3]</span><br></pre></td></tr></table></figure><p>Ring：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GPU0 → GPU1 → GPU2 → GPU3 → GPU0</span><br></pre></td></tr></table></figure><h3 id="Round-1"><a href="#Round-1" class="headerlink" title="Round 1"></a>Round 1</h3><p>各 GPU 同时发送一个不同的 chunk：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: a0 ─────────→ GPU1</span><br><span class="line">GPU1: b1 ─────────→ GPU2</span><br><span class="line">GPU2: c2 ─────────→ GPU3</span><br><span class="line">GPU3: d3 ─────────→ GPU0</span><br></pre></td></tr></table></figure><p>接收方拿到以后，与自己的同编号 chunk 做 reduction：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">GPU1:</span><br><span class="line"></span><br><span class="line">a0 + b0</span><br><span class="line"></span><br><span class="line">GPU2:</span><br><span class="line"></span><br><span class="line">b1 + c1</span><br><span class="line"></span><br><span class="line">GPU3:</span><br><span class="line"></span><br><span class="line">c2 + d2</span><br><span class="line"></span><br><span class="line">GPU0:</span><br><span class="line"></span><br><span class="line">d3 + a3</span><br></pre></td></tr></table></figure><p>因此现在 Ring 上出现了 4 个 partial sum：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">chunk0: a0 + b0</span><br><span class="line">chunk1: b1 + c1</span><br><span class="line">chunk2: c2 + d2</span><br><span class="line">chunk3: d3 + a3</span><br></pre></td></tr></table></figure><h3 id="Round-2"><a href="#Round-2" class="headerlink" title="Round 2"></a>Round 2</h3><p>这些 partial sum 继续向下一个 GPU 传：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU1 ── (a0+b0) ──→ GPU2</span><br><span class="line">GPU2 ── (b1+c1) ──→ GPU3</span><br><span class="line">GPU3 ── (c2+d2) ──→ GPU0</span><br><span class="line">GPU0 ── (d3+a3) ──→ GPU1</span><br></pre></td></tr></table></figure><p>每个接收 GPU 再把自己的对应 chunk 加进去：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">GPU2:</span><br><span class="line"></span><br><span class="line">a0 + b0 + c0</span><br><span class="line"></span><br><span class="line">GPU3:</span><br><span class="line"></span><br><span class="line">b1 + c1 + d1</span><br><span class="line"></span><br><span class="line">GPU0:</span><br><span class="line"></span><br><span class="line">c2 + d2 + a2</span><br><span class="line"></span><br><span class="line">GPU1:</span><br><span class="line"></span><br><span class="line">d3 + a3 + b3</span><br></pre></td></tr></table></figure><p>现在每个 partial sum 已经包含 3 个 GPU 的贡献。</p><h3 id="Round-3"><a href="#Round-3" class="headerlink" title="Round 3"></a>Round 3</h3><p>再向前传一次：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU2 ── (a0+b0+c0) ──→ GPU3</span><br><span class="line">GPU3 ── (b1+c1+d1) ──→ GPU0</span><br><span class="line">GPU0 ── (c2+d2+a2) ──→ GPU1</span><br><span class="line">GPU1 ── (d3+a3+b3) ──→ GPU2</span><br></pre></td></tr></table></figure><p>接收方加入自己的最后一部分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">GPU3:</span><br><span class="line"></span><br><span class="line">a0+b0+c0+d0 = S0</span><br><span class="line"></span><br><span class="line">GPU0:</span><br><span class="line"></span><br><span class="line">a1+b1+c1+d1 = S1</span><br><span class="line"></span><br><span class="line">GPU1:</span><br><span class="line"></span><br><span class="line">a2+b2+c2+d2 = S2</span><br><span class="line"></span><br><span class="line">GPU2:</span><br><span class="line"></span><br><span class="line">a3+b3+c3+d3 = S3</span><br></pre></td></tr></table></figure><p>于是 ReduceScatter 阶段完成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: S1</span><br><span class="line">GPU1: S2</span><br><span class="line">GPU2: S3</span><br><span class="line">GPU3: S0</span><br></pre></td></tr></table></figure><p>最重要的一点是：</p><blockquote><p>到这里 reduction 已经全部完成了，此时不需要再做任何加法。</p></blockquote><h2 id="6-第二阶段：Ring-AllGather"><a href="#6-第二阶段：Ring-AllGather" class="headerlink" title="6. 第二阶段：Ring AllGather"></a>6. 第二阶段：Ring AllGather</h2><p>现在 Ring 中分散着：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: S1</span><br><span class="line">GPU1: S2</span><br><span class="line">GPU2: S3</span><br><span class="line">GPU3: S0</span><br></pre></td></tr></table></figure><p>接下来只需要把这些完整的 reduced chunk 沿 Ring 传播。</p><p>注意：</p><ul><li>ReduceScatter 阶段：<code>receive + reduce + send</code></li><li>而 AllGather 阶段：<code>receive + copy + send</code>，<strong>不再进行 reduce</strong></li></ul><h3 id="AllGather-Round-1"><a href="#AllGather-Round-1" class="headerlink" title="AllGather Round 1"></a>AllGather Round 1</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 ── S1 ──→ GPU1</span><br><span class="line">GPU1 ── S2 ──→ GPU2</span><br><span class="line">GPU2 ── S3 ──→ GPU3</span><br><span class="line">GPU3 ── S0 ──→ GPU0</span><br></pre></td></tr></table></figure><p>现在每个 GPU 有两个结果 chunk。例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GPU0:</span><br><span class="line"></span><br><span class="line">S1 + S0</span><br></pre></td></tr></table></figure><h3 id="AllGather-Round-2"><a href="#AllGather-Round-2" class="headerlink" title="AllGather Round 2"></a>AllGather Round 2</h3><p>刚收到的数据继续向下传：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 ── S0 ──→ GPU1</span><br><span class="line">GPU1 ── S1 ──→ GPU2</span><br><span class="line">GPU2 ── S2 ──→ GPU3</span><br><span class="line">GPU3 ── S3 ──→ GPU0</span><br></pre></td></tr></table></figure><p>此时每个 GPU 有三个结果 chunk。</p><h3 id="AllGather-Round-3"><a href="#AllGather-Round-3" class="headerlink" title="AllGather Round 3"></a>AllGather Round 3</h3><p>最后再传播一轮：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 → GPU1</span><br><span class="line">GPU1 → GPU2</span><br><span class="line">GPU2 → GPU3</span><br><span class="line">GPU3 → GPU0</span><br></pre></td></tr></table></figure><p>最终：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: [S0, S1, S2, S3]</span><br><span class="line">GPU1: [S0, S1, S2, S3]</span><br><span class="line">GPU2: [S0, S1, S2, S3]</span><br><span class="line">GPU3: [S0, S1, S2, S3]</span><br></pre></td></tr></table></figure><p>这就是完整的 AllReduce。</p><h2 id="7-用一张图看完整-Ring-AllReduce"><a href="#7-用一张图看完整-Ring-AllReduce" class="headerlink" title="7. 用一张图看完整 Ring AllReduce"></a>7. 用一张图看完整 Ring AllReduce</h2><p>输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0: a0 a1 a2 a3</span><br><span class="line">GPU1: b0 b1 b2 b3</span><br><span class="line">GPU2: c0 c1 c2 c3</span><br><span class="line">GPU3: d0 d1 d2 d3</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line">          │</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line"></span><br><span class="line">=========================================</span><br><span class="line">          ReduceScatter</span><br><span class="line">=========================================</span><br><span class="line"></span><br><span class="line">数据沿 Ring 流动：</span><br><span class="line"></span><br><span class="line">GPU0 → GPU1 → GPU2 → GPU3 → GPU0</span><br><span class="line"></span><br><span class="line">每经过一个 GPU：</span><br><span class="line"></span><br><span class="line">partial_sum</span><br><span class="line">     +</span><br><span class="line">local_chunk</span><br><span class="line">     ↓</span><br><span class="line">new_partial_sum</span><br><span class="line"></span><br><span class="line">经过足够多的 GPU 后：</span><br><span class="line"></span><br><span class="line">GPU0: S1</span><br><span class="line">GPU1: S2</span><br><span class="line">GPU2: S3</span><br><span class="line">GPU3: S0</span><br><span class="line"></span><br><span class="line">其中：</span><br><span class="line"></span><br><span class="line">S0 = a0+b0+c0+d0</span><br><span class="line">S1 = a1+b1+c1+d1</span><br><span class="line">S2 = a2+b2+c2+d2</span><br><span class="line">S3 = a3+b3+c3+d3</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">          │</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line"></span><br><span class="line">=========================================</span><br><span class="line">             AllGather</span><br><span class="line">=========================================</span><br><span class="line"></span><br><span class="line">S0、S1、S2、S3 已经全部算好。</span><br><span class="line"></span><br><span class="line">它们继续沿 Ring 传播：</span><br><span class="line"></span><br><span class="line">GPU0 → GPU1 → GPU2 → GPU3 → GPU0</span><br><span class="line"></span><br><span class="line">只：</span><br><span class="line"></span><br><span class="line">recv + copy + send</span><br><span class="line"></span><br><span class="line">不再 reduce。</span><br><span class="line"></span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line"></span><br><span class="line">GPU0: S0 S1 S2 S3</span><br><span class="line">GPU1: S0 S1 S2 S3</span><br><span class="line">GPU2: S0 S1 S2 S3</span><br><span class="line">GPU3: S0 S1 S2 S3</span><br></pre></td></tr></table></figure><p>所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">                  AllReduce</span><br><span class="line"></span><br><span class="line">                      │</span><br><span class="line">         ┌────────────┴────────────┐</span><br><span class="line">         │                         │</span><br><span class="line">         ▼                         ▼</span><br><span class="line"></span><br><span class="line">   ReduceScatter               AllGather</span><br><span class="line"></span><br><span class="line">recv + reduce + send       recv + copy + send</span><br><span class="line">         │                         │</span><br><span class="line">         ▼                         ▼</span><br><span class="line">每人得到一个完整结果块      每人得到所有结果块</span><br><span class="line"></span><br><span class="line">         └────────────┬────────────┘</span><br><span class="line">                      │</span><br><span class="line">                      ▼</span><br><span class="line"></span><br><span class="line">              完整 AllReduce 结果</span><br></pre></td></tr></table></figure><h2 id="8-Fig-4-中-NCCL-primitive-到底是什么意思？"><a href="#8-Fig-4-中-NCCL-primitive-到底是什么意思？" class="headerlink" title="8. Fig. 4 中 NCCL primitive 到底是什么意思？"></a>8. Fig. 4 中 NCCL primitive 到底是什么意思？</h2><p>论文没有只停留在”ReduceScatter + AllGather”这个抽象描述，而是进一步展示了 NCCL Ring AllReduce 内部使用的 primitive。</p><p>对于 k 个 GPU，论文 Table V 给出的一个 loop iteration 是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Step 0:      send</span><br><span class="line"></span><br><span class="line">Step 1 ~ k-2:   recvReduceSend</span><br><span class="line"></span><br><span class="line">Step k-1:       recvReduceCopySend</span><br><span class="line"></span><br><span class="line">Step k ~ 2k-3:  recvCopySend</span><br><span class="line"></span><br><span class="line">Step 2k-2:      recv</span><br></pre></td></tr></table></figure><p>对于 4 个 GPU：k &#x3D; 4，因此是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Step 0: send</span><br><span class="line"></span><br><span class="line">Step 1: recvReduceSend</span><br><span class="line">Step 2: recvReduceSend</span><br><span class="line"></span><br><span class="line">Step 3: recvReduceCopySend</span><br><span class="line"></span><br><span class="line">Step 4: recvCopySend</span><br><span class="line">Step 5: recvCopySend</span><br><span class="line"></span><br><span class="line">Step 6: recv</span><br></pre></td></tr></table></figure><p>总共：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">2k - 1</span><br><span class="line">=</span><br><span class="line">7 primitive steps</span><br></pre></td></tr></table></figure><h2 id="9-这些-primitive-分别在干什么？"><a href="#9-这些-primitive-分别在干什么？" class="headerlink" title="9. 这些 primitive 分别在干什么？"></a>9. 这些 primitive 分别在干什么？</h2><h3 id="send"><a href="#send" class="headerlink" title="send"></a>send</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">读取本地 chunk</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">发送给下一个 GPU</span><br></pre></td></tr></table></figure><p>用于启动 Ring 中的数据流动。</p><h3 id="recvReduceSend"><a href="#recvReduceSend" class="headerlink" title="recvReduceSend"></a>recvReduceSend</h3><p>这是 ReduceScatter 阶段最关键的 primitive：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">从前一个 GPU 收数据</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line">        recv</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line">和自己的 local chunk 做 reduction</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line">       reduce</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line">把 partial result 发给下一个 GPU</span><br><span class="line">          │</span><br><span class="line">          ▼</span><br><span class="line">        send</span><br></pre></td></tr></table></figure><p>也就是：<code>recv + reduce + send</code>。数据每经过一个 GPU，就多融合一份输入。</p><h3 id="recvReduceCopySend"><a href="#recvReduceCopySend" class="headerlink" title="recvReduceCopySend"></a>recvReduceCopySend</h3><p>这是 Fig. 4 里最值得注意的一步。它位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ReduceScatter</span><br><span class="line">      ↓</span><br><span class="line">    边界</span><br><span class="line">      ↓</span><br><span class="line">AllGather</span><br></pre></td></tr></table></figure><p>这个 primitive 同时做：</p><ul><li><code>recv</code></li><li>最后一次 <code>reduce</code></li><li>copy 到自己的 output buffer</li><li>把已经完全 reduce 好的 chunk 发给下一张 GPU</li></ul><p>也就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">recv</span><br><span class="line">   ↓</span><br><span class="line">final reduce</span><br><span class="line">   ↓</span><br><span class="line">得到完整 Sx</span><br><span class="line">   ├────────→ copy 到本地 output</span><br><span class="line">   │</span><br><span class="line">   └────────→ send 给下一 GPU</span><br></pre></td></tr></table></figure><p>因此它实际上把两个阶段的边界连接起来了：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ReduceScatter 最后一步</span><br><span class="line">        +</span><br><span class="line">AllGather 第一次传播</span><br></pre></td></tr></table></figure><h3 id="recvCopySend"><a href="#recvCopySend" class="headerlink" title="recvCopySend"></a>recvCopySend</h3><p>进入 AllGather 后已经没有 reduction，所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">recv</span><br><span class="line"> ↓</span><br><span class="line">copy 到自己的 output buffer</span><br><span class="line"> ↓</span><br><span class="line">send 给下一 GPU</span><br></pre></td></tr></table></figure><p>即：<code>recv + copy + send</code>。这里传播的是已经完整计算好的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">S0 / S1 / S2 / S3</span><br></pre></td></tr></table></figure><h3 id="recv"><a href="#recv" class="headerlink" title="recv"></a>recv</h3><p>最后一块数据只需要接收并放到对应位置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">recv</span><br><span class="line"> ↓</span><br><span class="line">AllReduce 完成</span><br></pre></td></tr></table></figure><p>此时所有 GPU 都拥有完整结果。</p><h2 id="10-为什么-Fig-4-是-2k-1-个-step，而我们常说两个阶段各-k-1-轮？"><a href="#10-为什么-Fig-4-是-2k-1-个-step，而我们常说两个阶段各-k-1-轮？" class="headerlink" title="10. 为什么 Fig. 4 是 2k-1 个 step，而我们常说两个阶段各 k-1 轮？"></a>10. 为什么 Fig. 4 是 2k-1 个 step，而我们常说两个阶段各 k-1 轮？</h2><p>这里非常容易混淆。</p><p>从算法语义 &#x2F; 通信轮次理解 Ring AllReduce：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ReduceScatter : k - 1 rounds</span><br><span class="line">AllGather     : k - 1 rounds</span><br></pre></td></tr></table></figure><p>因此通常写成 <code>2(k - 1)</code> 个 Ring 通信轮次。</p><p>但 Fig. 4 分析的是 NCCL 内部 primitive 的执行序列。它把：</p><ul><li>最开始的 <code>send</code></li><li>最后的 <code>recv</code></li></ul><p>也分别列成 primitive step，并且在两个阶段的交界处使用 <code>recvReduceCopySend</code>，把 final reduction + copy + 下一阶段的 send 融合起来。</p><p>所以论文的 primitive 计数是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1 + (k - 2) + 1 + (k - 2) + 1 = 2k - 1</span><br></pre></td></tr></table></figure><p>这和 “ReduceScatter + AllGather” 并不矛盾——两者描述的粒度不同：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">k-1 + k-1   描述：算法层面的 Ring rounds</span><br><span class="line"></span><br><span class="line">2k-1        描述：Fig.4 中 NCCL primitive sequence</span><br></pre></td></tr></table></figure><h2 id="11-所以”AllReduce-ReduceScatter-AllGather”应该怎么准确理解？"><a href="#11-所以”AllReduce-ReduceScatter-AllGather”应该怎么准确理解？" class="headerlink" title="11. 所以”AllReduce &#x3D; ReduceScatter + AllGather”应该怎么准确理解？"></a>11. 所以”AllReduce &#x3D; ReduceScatter + AllGather”应该怎么准确理解？</h2><p>最准确的说法不是”NCCL 内部一定真的调用 <code>ncclReduceScatter(...)</code> 然后 <code>ncclAllGather(...)</code>“，而应该说：</p><blockquote><p>从 collective 的数据语义来看，AllReduce 可以分解为 ReduceScatter 和 AllGather；Ring AllReduce 的实现也确实表现为前半段进行 ReduceScatter-like 的逐块归约，后半段进行 AllGather-like 的结果传播。</p></blockquote><p>NCCL 的 Ring 实现会进一步做 primitive 融合（<code>recvReduceCopySend</code>），所以实际执行不是两个完全独立的 collective kernel 简单拼接。</p><p>可以理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">理论 / 语义：</span><br><span class="line"></span><br><span class="line">AllReduce = ReduceScatter + AllGather</span><br><span class="line"></span><br><span class="line">而 NCCL Ring 实现：</span><br><span class="line"></span><br><span class="line">send</span><br><span class="line">  ↓</span><br><span class="line">recvReduceSend</span><br><span class="line">  ↓</span><br><span class="line">recvReduceSend</span><br><span class="line">  ↓</span><br><span class="line">recvReduceCopySend     ← 两阶段边界被融合</span><br><span class="line">  ↓</span><br><span class="line">recvCopySend</span><br><span class="line">  ↓</span><br><span class="line">recvCopySend</span><br><span class="line">  ↓</span><br><span class="line">recv</span><br></pre></td></tr></table></figure><h2 id="12-为什么-Ring-要把数据切成-chunk？"><a href="#12-为什么-Ring-要把数据切成-chunk？" class="headerlink" title="12. 为什么 Ring 要把数据切成 chunk？"></a>12. 为什么 Ring 要把数据切成 chunk？</h2><p>如果整个 tensor 都由一个 GPU 收集并做 reduction：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GPU0 ← GPU1</span><br><span class="line">GPU0 ← GPU2</span><br><span class="line">GPU0 ← GPU3</span><br></pre></td></tr></table></figure><p>GPU0 很容易成为瓶颈。</p><p>Ring 的做法是把 N 个元素切成 k 份：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Tensor:</span><br><span class="line"></span><br><span class="line">┌──────┬──────┬──────┬──────┐</span><br><span class="line">│ C0   │ C1   │ C2   │ C3   │</span><br><span class="line">└──────┴──────┴──────┴──────┘</span><br></pre></td></tr></table></figure><p>让所有 GPU 同时 <code>send + recv + reduce</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 → GPU1</span><br><span class="line">GPU1 → GPU2</span><br><span class="line">GPU2 → GPU3</span><br><span class="line">GPU3 → GPU0</span><br></pre></td></tr></table></figure><p>四条链路可以同时工作。</p><p>因此不存在唯一的 reduction root，不同 chunk 的最终 reduction 分散到不同 GPU：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">GPU0 负责一个 chunk</span><br><span class="line">GPU1 负责一个 chunk</span><br><span class="line">GPU2 负责一个 chunk</span><br><span class="line">GPU3 负责一个 chunk</span><br></pre></td></tr></table></figure><p>这正是 ReduceScatter 的含义。然后再利用同一个 Ring 做 AllGather。</p><h2 id="13-Ring-AllReduce-的通信量"><a href="#13-Ring-AllReduce-的通信量" class="headerlink" title="13. Ring AllReduce 的通信量"></a>13. Ring AllReduce 的通信量</h2><p>假设每个 GPU 上的数据总量为 N bytes，一共有 k 个 GPU，每个 chunk 大约是 N&#x2F;k。</p><p>ReduceScatter 有 k-1 轮，每一轮每个 GPU 发送一个 chunk：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ReduceScatter 每 GPU 发送量 = (k-1) × N/k = N(k-1)/k</span><br></pre></td></tr></table></figure><p>AllGather 同样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">AllGather 每 GPU 发送量 = N(k-1)/k</span><br></pre></td></tr></table></figure><p>因此 Ring AllReduce 每个 GPU 总发送量约为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2N(k-1)/k</span><br></pre></td></tr></table></figure><p>当 GPU 数量很大时，(k-1)&#x2F;k ≈ 1，所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">总发送量 ≈ 2N</span><br></pre></td></tr></table></figure><p>这也是 Ring AllReduce 能较好利用链路带宽的重要原因之一：所有 GPU 都持续承担通信，而不是让一个 root 承担全部数据传输。</p><h2 id="14-最后总结"><a href="#14-最后总结" class="headerlink" title="14. 最后总结"></a>14. 最后总结</h2><p>一句话：</p><blockquote><p>ReduceScatter 负责”把每个结果 chunk 算完整”，AllGather 负责”让每个 GPU 都拿到这些已经算完整的 chunk”，两步接起来后，每个 GPU 都得到完整 reduction 结果，所以等价于 AllReduce。</p></blockquote><p>结合 Fig. 4：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">           Ring AllReduce</span><br><span class="line"></span><br><span class="line">                │</span><br><span class="line">                ▼</span><br><span class="line"></span><br><span class="line">┌─────────────────────────────┐</span><br><span class="line">│       ReduceScatter-like    │</span><br><span class="line">│                             │</span><br><span class="line">│ send                        │</span><br><span class="line">│ recvReduceSend              │</span><br><span class="line">│ recvReduceSend              │</span><br><span class="line">│                             │</span><br><span class="line">│ partial sum 不断沿 Ring     │</span><br><span class="line">│ 流动并累加                   │</span><br><span class="line">└──────────────┬──────────────┘</span><br><span class="line">               │</span><br><span class="line">               │ recvReduceCopySend</span><br><span class="line">               │</span><br><span class="line">               │ 最后一次 reduce</span><br><span class="line">               │ + copy</span><br><span class="line">               │ + send</span><br><span class="line">               ▼</span><br><span class="line">┌─────────────────────────────┐</span><br><span class="line">│        AllGather-like       │</span><br><span class="line">│                             │</span><br><span class="line">│ recvCopySend                │</span><br><span class="line">│ recvCopySend                │</span><br><span class="line">│ recv                        │</span><br><span class="line">│                             │</span><br><span class="line">│ 完整 reduced chunk 沿 Ring  │</span><br><span class="line">│ 传播，不再做 reduction       │</span><br><span class="line">└──────────────┬──────────────┘</span><br><span class="line">               │</span><br><span class="line">               ▼</span><br><span class="line"></span><br><span class="line"> 每个 GPU 都得到完整 reduction result</span><br></pre></td></tr></table></figure><p>所以最终可以记成：</p><blockquote><p><strong>AllReduce &#x3D; ReduceScatter + AllGather</strong></p></blockquote><p>但在 NCCL Ring 的实际实现中，更准确地说是：</p><blockquote><p><strong>AllReduce &#x3D; ReduceScatter-like reduction phase + AllGather-like dissemination phase</strong></p></blockquote><p>并且两阶段的边界通过 <code>recvReduceCopySend</code> 进行了融合。</p>]]>
    </content>
    <id>https://loosp.cn/2026/09/10/allreduce-reducescatter-allgather-nccl/</id>
    <link href="https://loosp.cn/2026/09/10/allreduce-reducescatter-allgather-nccl/"/>
    <published>2026-09-10T02:00:00.000Z</published>
    <summary>从语义与 NCCL Ring 实现两个层面，拆解 AllReduce = ReduceScatter + AllGather 的由来与 primitive 序列。</summary>
    <title>为什么说 AllReduce = ReduceScatter + AllGather？——结合 NCCL Ring AllReduce</title>
    <updated>2026-09-08T20:55:13.908Z</updated>
  </entry>
  <entry>
    <author>
      <name>loosp</name>
    </author>
    <category term="学习" scheme="https://loosp.cn/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="学习笔记" scheme="https://loosp.cn/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <category term="rdma" scheme="https://loosp.cn/tags/rdma/"/>
    <category term="infiniband" scheme="https://loosp.cn/tags/infiniband/"/>
    <category term="network" scheme="https://loosp.cn/tags/network/"/>
    <content>
      <![CDATA[<h1 id="RDMA-单边操作：QP、WR-WQE-与-RDMA-WRITE-PUT-流程"><a href="#RDMA-单边操作：QP、WR-WQE-与-RDMA-WRITE-PUT-流程" class="headerlink" title="RDMA 单边操作：QP、WR&#x2F;WQE 与 RDMA WRITE &#x2F; PUT 流程"></a>RDMA 单边操作：QP、WR&#x2F;WQE 与 RDMA WRITE &#x2F; PUT 流程</h1><h2 id="1-什么叫单边-RDMA？"><a href="#1-什么叫单边-RDMA？" class="headerlink" title="1. 什么叫单边 RDMA？"></a>1. 什么叫单边 RDMA？</h2><p><strong>单边 RDMA（One-sided RDMA）</strong> 指的是：</p><blockquote><p>发起方可以直接读写远端机器已经注册好的内存，而远端 CPU 不需要为这次数据传输主动执行 <code>recv()</code>、<code>read()</code> 或 <code>write()</code>。</p></blockquote><p>典型的单边操作有：</p><ul><li><code>RDMA WRITE</code>：把本地内存的数据写到远端内存</li><li><code>RDMA READ</code>：把远端内存的数据读到本地内存</li><li>Atomic：远端原子操作，例如 Compare-and-Swap、Fetch-and-Add</li></ul><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Host A                                      Host B</span><br><span class="line"></span><br><span class="line">CPU / GPU                                   CPU</span><br><span class="line">   │</span><br><span class="line">   │ RDMA WRITE</span><br><span class="line">   ▼</span><br><span class="line"> RNIC A =================================&gt; RNIC B</span><br><span class="line">                                             │</span><br><span class="line">                                             │ DMA</span><br><span class="line">                                             ▼</span><br><span class="line">                                      Remote Memory</span><br></pre></td></tr></table></figure><p>假设 B 已经有一块内存：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0x70000000 ~ 0x70000fff</span><br></pre></td></tr></table></figure><p>B 先把这块内存注册给 RNIC，并把：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">remote_addr = 0x70000000</span><br><span class="line">rkey        = 0x123456</span><br></pre></td></tr></table></figure><p>告诉 A。</p><p>之后 A 就可以发起：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE</span><br></pre></td></tr></table></figure><p>让自己的 RNIC 直接把数据写进：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B: 0x70000000</span><br></pre></td></tr></table></figure><p>整个数据传输过程中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B CPU 不需要调用 recv()</span><br></pre></td></tr></table></figure><p>这就是所谓的 <strong>one-sided</strong>。</p><hr><h2 id="单边和双边的区别"><a href="#单边和双边的区别" class="headerlink" title="单边和双边的区别"></a>单边和双边的区别</h2><p>双边操作典型是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SEND / RECV</span><br></pre></td></tr></table></figure><p>要求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">A:</span><br><span class="line">post_send()</span><br><span class="line"></span><br><span class="line">B:</span><br><span class="line">post_recv()</span><br></pre></td></tr></table></figure><p>也就是说双方都要参与。</p><p>流程类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">A SQ                        B RQ</span><br><span class="line"></span><br><span class="line">SEND WQE  -------------&gt;   RECV WQE</span><br><span class="line">                               │</span><br><span class="line">                               ▼</span><br><span class="line">                            B Buffer</span><br></pre></td></tr></table></figure><p>而单边：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">A SQ                      B Memory</span><br><span class="line"></span><br><span class="line">RDMA WRITE WQE ---------&gt; 直接写入</span><br></pre></td></tr></table></figure><p>B 不需要为这一条 WRITE 提前放一个 Receive WQE。</p><p>所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SEND/RECV = two-sided</span><br><span class="line"></span><br><span class="line">READ/WRITE = one-sided</span><br></pre></td></tr></table></figure><hr><h1 id="2-什么是-QP？"><a href="#2-什么是-QP？" class="headerlink" title="2. 什么是 QP？"></a>2. 什么是 QP？</h1><p>QP 全称：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Queue Pair</span><br></pre></td></tr></table></figure><p>也就是：</p><blockquote><p>一对 Work Queue。</p></blockquote><p>一个 QP 最基本包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">          QP</span><br><span class="line"></span><br><span class="line">┌─────────────────────┐</span><br><span class="line">│                     │</span><br><span class="line">│   SQ          RQ    │</span><br><span class="line">│                     │</span><br><span class="line">│ Send Queue Receive  │</span><br><span class="line">│            Queue    │</span><br><span class="line">└─────────────────────┘</span><br></pre></td></tr></table></figure><p>即：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP = SQ + RQ</span><br></pre></td></tr></table></figure><p>其中：</p><h3 id="SQ：Send-Queue"><a href="#SQ：Send-Queue" class="headerlink" title="SQ：Send Queue"></a>SQ：Send Queue</h3><p>放主动发起的任务。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SEND</span><br><span class="line">RDMA WRITE</span><br><span class="line">RDMA READ</span><br><span class="line">Atomic</span><br></pre></td></tr></table></figure><p>这些 WR 最终都会进入 SQ。</p><hr><h3 id="RQ：Receive-Queue"><a href="#RQ：Receive-Queue" class="headerlink" title="RQ：Receive Queue"></a>RQ：Receive Queue</h3><p>主要服务于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SEND / RECV</span><br></pre></td></tr></table></figure><p>例如 B 希望接收 A 发来的 SEND：</p><p>B 需要提前：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">post_recv()</span><br></pre></td></tr></table></figure><p>放一个 Receive WQE 到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B.RQ</span><br></pre></td></tr></table></figure><hr><h1 id="3-CQ-是什么？"><a href="#3-CQ-是什么？" class="headerlink" title="3. CQ 是什么？"></a>3. CQ 是什么？</h1><p>除了 QP，还通常会有：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CQ</span><br><span class="line">Completion Queue</span><br></pre></td></tr></table></figure><p>CQ 用来告诉软件：</p><blockquote><p>某个 Work Request 完成了。</p></blockquote><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">A CPU</span><br><span class="line"> │</span><br><span class="line"> │ ibv_post_send()</span><br><span class="line"> ▼</span><br><span class="line">A SQ</span><br><span class="line"> │</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">RNIC</span><br><span class="line"> │</span><br><span class="line"> │ RDMA WRITE</span><br><span class="line"> ▼</span><br><span class="line">Network</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">B Memory</span><br><span class="line"></span><br><span class="line">完成以后</span><br><span class="line"></span><br><span class="line">A RNIC</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">A CQ</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">WC</span><br><span class="line">Work Completion</span><br></pre></td></tr></table></figure><p>CPU 可以：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ibv_poll_cq(...)</span><br></pre></td></tr></table></figure><p>检查完成状态。</p><p>因此常见关系是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">        QP</span><br><span class="line"> ┌──────────────┐</span><br><span class="line"> │              │</span><br><span class="line">SQ              RQ</span><br><span class="line"> │              │</span><br><span class="line"> └──────┬───────┘</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">        CQ</span><br></pre></td></tr></table></figure><p>严格来说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CQ 不属于 QP</span><br></pre></td></tr></table></figure><p>而是 QP 在创建时指定自己的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">send_cq</span><br><span class="line">recv_cq</span><br></pre></td></tr></table></figure><p>多个 QP 甚至可以共享一个 CQ。</p><hr><h1 id="4-单边-RDMA-需要-RQ-吗？"><a href="#4-单边-RDMA-需要-RQ-吗？" class="headerlink" title="4. 单边 RDMA 需要 RQ 吗？"></a>4. 单边 RDMA 需要 RQ 吗？</h1><p>这是一个非常重要的问题。</p><p>对于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE</span><br><span class="line">RDMA READ</span><br></pre></td></tr></table></figure><p><strong>数据传输本身不需要远端 RQ。</strong></p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">A                                   B</span><br><span class="line"></span><br><span class="line">SQ                                  RQ</span><br><span class="line">│                                   │</span><br><span class="line">│ RDMA WRITE                         │</span><br><span class="line">│                                   │</span><br><span class="line">└───────────────&gt; B memory           │</span><br><span class="line">                                    X</span><br><span class="line">                              不消费 Receive WQE</span><br></pre></td></tr></table></figure><p>因此可以说：</p><blockquote><p>RDMA WRITE &#x2F; READ 不依赖远端 RQ 中的 Receive WQE。</p></blockquote><p>但不能简单地说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">单边 RDMA 的 QP 没有 RQ</span><br></pre></td></tr></table></figure><p>因为从 QP 结构上：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP = SQ + RQ</span><br></pre></td></tr></table></figure><p>RQ 仍然是 QP 的组成部分。</p><p>只是在纯单边通信场景下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RQ 可以基本不用。</span><br></pre></td></tr></table></figure><p>所以更准确的说法是：</p><blockquote><p>单边 RDMA 操作仍然通过 QP 发起，但 RDMA READ&#x2F;WRITE 不需要远端提前 post receive，因此远端 RQ 不参与这次数据搬运。</p></blockquote><hr><h1 id="5-单边-RDMA-是不是只需要-SQ-CQ？"><a href="#5-单边-RDMA-是不是只需要-SQ-CQ？" class="headerlink" title="5. 单边 RDMA 是不是只需要 SQ + CQ？"></a>5. 单边 RDMA 是不是只需要 SQ + CQ？</h1><p>从<strong>操作路径</strong>来看，可以近似理解成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Local:</span><br><span class="line"></span><br><span class="line">SQ</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">RNIC</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">Remote Memory</span><br><span class="line"></span><br><span class="line">+</span><br><span class="line">CQ 返回完成结果</span><br></pre></td></tr></table></figure><p>因此对于纯 RDMA WRITE：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">A SQ</span><br><span class="line">A CQ</span><br><span class="line"></span><br><span class="line">B 注册好的 Memory Region</span><br></pre></td></tr></table></figure><p>基本就足够完成数据传输了。</p><p>不过还是要注意：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">RQ 是 QP 的一部分，</span><br><span class="line">只是这次 RDMA WRITE 没有用它。</span><br></pre></td></tr></table></figure><hr><h1 id="6-WR-和-WQE-是什么关系？"><a href="#6-WR-和-WQE-是什么关系？" class="headerlink" title="6. WR 和 WQE 是什么关系？"></a>6. WR 和 WQE 是什么关系？</h1><p>软件层面通常操作的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">WR</span><br><span class="line">Work Request</span><br></pre></td></tr></table></figure><p>例如 libibverbs 中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ibv_send_wr</span></span></span><br></pre></td></tr></table></figure><p>应用程序构造 WR：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">WR</span><br><span class="line">│</span><br><span class="line">│ ibv_post_send()</span><br><span class="line">▼</span><br><span class="line">QP Send Queue</span><br><span class="line">│</span><br><span class="line">▼</span><br><span class="line">WQE</span><br></pre></td></tr></table></figure><p>可以简单理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">WR = 软件告诉 RDMA 驱动&quot;我要做什么&quot;</span><br><span class="line"></span><br><span class="line">WQE = RNIC 真正能够读取和执行的队列项</span><br></pre></td></tr></table></figure><p>概念上：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Application</span><br><span class="line">    │</span><br><span class="line">    │ WR</span><br><span class="line">    ▼</span><br><span class="line">libibverbs / Driver</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">   WQE</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">   SQ</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">  RNIC</span><br></pre></td></tr></table></figure><hr><h1 id="7-我要发一个-RDMA-WRITE，WR-里要填什么？"><a href="#7-我要发一个-RDMA-WRITE，WR-里要填什么？" class="headerlink" title="7. 我要发一个 RDMA WRITE，WR 里要填什么？"></a>7. 我要发一个 RDMA WRITE，WR 里要填什么？</h1><p>假设：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">A 要把自己的 buffer 写到 B 的 buffer。</span><br></pre></td></tr></table></figure><p>基本需要两类信息：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">本地信息</span><br><span class="line">+</span><br><span class="line">远端信息</span><br></pre></td></tr></table></figure><hr><h2 id="7-1-本地信息"><a href="#7-1-本地信息" class="headerlink" title="7.1 本地信息"></a>7.1 本地信息</h2><p>A 需要告诉 RNIC：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">数据在哪？</span><br><span class="line">数据多长？</span><br><span class="line">这块内存是否有权限访问？</span><br></pre></td></tr></table></figure><p>对应一个 SGE：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ibv_sge</span> &#123;</span></span><br><span class="line">    <span class="type">uint64_t</span> addr;</span><br><span class="line">    <span class="type">uint32_t</span> length;</span><br><span class="line">    <span class="type">uint32_t</span> lkey;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>也就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">addr</span><br><span class="line">length</span><br><span class="line">lkey</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">A local buffer:</span><br><span class="line"></span><br><span class="line">addr   = 0x10000000</span><br><span class="line">length = 4096</span><br><span class="line">lkey   = 0xaaaa</span><br></pre></td></tr></table></figure><p>含义：</p><h3 id="addr"><a href="#addr" class="headerlink" title="addr"></a>addr</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">本地数据起始地址</span><br></pre></td></tr></table></figure><h3 id="length"><a href="#length" class="headerlink" title="length"></a>length</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">传多少字节</span><br></pre></td></tr></table></figure><h3 id="lkey"><a href="#lkey" class="headerlink" title="lkey"></a>lkey</h3><p>RNIC 用来验证：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这个 QP 是否有权限 DMA 访问这块本地内存</span><br></pre></td></tr></table></figure><hr><h1 id="8-为什么要有-lkey？"><a href="#8-为什么要有-lkey？" class="headerlink" title="8. 为什么要有 lkey？"></a>8. 为什么要有 lkey？</h1><p>因为不能让 RNIC 随便 DMA 任意虚拟地址。</p><p>应用需要先：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ibv_reg_mr(...)</span><br></pre></td></tr></table></figure><p>注册 Memory Region：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Memory</span><br><span class="line">   │</span><br><span class="line">   │ ibv_reg_mr</span><br><span class="line">   ▼</span><br><span class="line">   MR</span><br><span class="line"> ┌───────────┐</span><br><span class="line"> │ addr      │</span><br><span class="line"> │ length    │</span><br><span class="line"> │ lkey      │</span><br><span class="line"> │ rkey      │</span><br><span class="line"> └───────────┘</span><br></pre></td></tr></table></figure><p>其中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lkey</span><br></pre></td></tr></table></figure><p>主要用于本地 RNIC 访问本地 Memory Region。</p><hr><h1 id="9-RDMA-WRITE-还需要远端信息"><a href="#9-RDMA-WRITE-还需要远端信息" class="headerlink" title="9. RDMA WRITE 还需要远端信息"></a>9. RDMA WRITE 还需要远端信息</h1><p>A 还必须知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">B.remote_addr</span><br><span class="line">B.rkey</span><br></pre></td></tr></table></figure><p>WR 中类似：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">wr.wr.rdma.remote_addr = remote_addr;</span><br><span class="line">wr.wr.rdma.rkey        = rkey;</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE WQE</span><br><span class="line"></span><br><span class="line">┌─────────────────────────┐</span><br><span class="line">│ opcode = RDMA_WRITE     │</span><br><span class="line">│                         │</span><br><span class="line">│ local_addr              │</span><br><span class="line">│ local_length            │</span><br><span class="line">│ local_lkey              │</span><br><span class="line">│                         │</span><br><span class="line">│ remote_addr             │</span><br><span class="line">│ remote_rkey             │</span><br><span class="line">└─────────────────────────┘</span><br></pre></td></tr></table></figure><hr><h1 id="10-还要填什么？"><a href="#10-还要填什么？" class="headerlink" title="10. 还要填什么？"></a>10. 还要填什么？</h1><p>一个典型 Send WR 还会包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">wr_id</span><br><span class="line">opcode</span><br><span class="line">send_flags</span><br><span class="line">SGE</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ibv_send_wr</span> <span class="title">wr</span> =</span> &#123;&#125;;</span><br><span class="line"></span><br><span class="line">wr.wr_id      = <span class="number">123</span>;</span><br><span class="line">wr.opcode     = IBV_WR_RDMA_WRITE;</span><br><span class="line">wr.sg_list    = &amp;sge;</span><br><span class="line">wr.num_sge    = <span class="number">1</span>;</span><br><span class="line">wr.send_flags = IBV_SEND_SIGNALED;</span><br><span class="line"></span><br><span class="line">wr.wr.rdma.remote_addr = remote_addr;</span><br><span class="line">wr.wr.rdma.rkey        = rkey;</span><br></pre></td></tr></table></figure><p>因此可以总结成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">WR</span><br><span class="line">│</span><br><span class="line">├── 做什么？</span><br><span class="line">│    └── opcode</span><br><span class="line">│</span><br><span class="line">├── 本地数据在哪？</span><br><span class="line">│    ├── local_addr</span><br><span class="line">│    ├── length</span><br><span class="line">│    └── lkey</span><br><span class="line">│</span><br><span class="line">├── 远端写到哪？</span><br><span class="line">│    ├── remote_addr</span><br><span class="line">│    └── rkey</span><br><span class="line">│</span><br><span class="line">├── 完成以后怎么识别？</span><br><span class="line">│    └── wr_id</span><br><span class="line">│</span><br><span class="line">└── 是否产生 CQE？</span><br><span class="line">     └── send_flags</span><br></pre></td></tr></table></figure><hr><h1 id="11-接收方-B-要提供什么？"><a href="#11-接收方-B-要提供什么？" class="headerlink" title="11. 接收方 B 要提供什么？"></a>11. 接收方 B 要提供什么？</h1><p>这是单边 RDMA 中最容易问到的问题。</p><p>对于 RDMA WRITE，B 最重要的是提供：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><p>通常流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">B:</span><br><span class="line"></span><br><span class="line">malloc buffer</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">ibv_reg_mr()</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">得到 MR</span><br><span class="line">    │</span><br><span class="line">    ├── addr</span><br><span class="line">    └── rkey</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">B ----------------------&gt; A</span><br><span class="line">     remote_addr + rkey</span><br></pre></td></tr></table></figure><p>这种信息交换本身通常走：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">TCP</span><br><span class="line">RDMA SEND/RECV</span><br><span class="line">RPC</span><br><span class="line">控制面协议</span><br></pre></td></tr></table></figure><p>而不是”凭空知道”。</p><hr><h1 id="12-B-要不要把-length-给-A？"><a href="#12-B-要不要把-length-给-A？" class="headerlink" title="12. B 要不要把 length 给 A？"></a>12. B 要不要把 length 给 A？</h1><p>从 RDMA WRITE WQE 本身来说：</p><blockquote><p>A 写多少字节，是由 A 本地 SGE 中的 <code>length</code> 决定的。</p></blockquote><p>因此 WQE 里的远端部分主要是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><p>没有一个：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">remote_length</span><br></pre></td></tr></table></figure><p>字段。</p><p>但是实际协议里，B 通常仍然需要告诉 A：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这块 buffer 有多大。</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">B 告诉 A：</span><br><span class="line"></span><br><span class="line">remote_addr = 0x70000000</span><br><span class="line">rkey        = 1234</span><br><span class="line">capacity    = 4096</span><br></pre></td></tr></table></figure><p>这样 A 才知道不能写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8192 bytes</span><br></pre></td></tr></table></figure><p>否则可能越界。</p><p>所以需要区分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">硬件 WQE 必须字段：</span><br><span class="line"></span><br><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><p>和：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">软件协议通常也会交换：</span><br><span class="line"></span><br><span class="line">buffer length / capacity</span><br></pre></td></tr></table></figure><hr><h1 id="13-把-WR-转成一个具体例子"><a href="#13-把-WR-转成一个具体例子" class="headerlink" title="13. 把 WR 转成一个具体例子"></a>13. 把 WR 转成一个具体例子</h1><p>假设：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">A local buffer:</span><br><span class="line"></span><br><span class="line">0x10000000</span><br><span class="line">4 KB</span><br><span class="line"></span><br><span class="line">B remote buffer:</span><br><span class="line"></span><br><span class="line">0x70000000</span><br><span class="line">4 KB</span><br></pre></td></tr></table></figure><p>A 有：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">local_addr = 0x10000000</span><br><span class="line">length     = 4096</span><br><span class="line">lkey       = 0x1111</span><br></pre></td></tr></table></figure><p>B 给 A：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">remote_addr = 0x70000000</span><br><span class="line">rkey        = 0x2222</span><br></pre></td></tr></table></figure><p>于是 A 创建：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">                   RDMA WRITE WR</span><br><span class="line"></span><br><span class="line">┌────────────────────────────────────────┐</span><br><span class="line">│ opcode      = RDMA_WRITE              │</span><br><span class="line">│                                        │</span><br><span class="line">│ local addr  = 0x10000000              │</span><br><span class="line">│ length      = 4096                    │</span><br><span class="line">│ lkey        = 0x1111                  │</span><br><span class="line">│                                        │</span><br><span class="line">│ remote addr = 0x70000000              │</span><br><span class="line">│ rkey        = 0x2222                  │</span><br><span class="line">└────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>提交：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ibv_post_send(qp, &amp;wr, &amp;bad_wr);</span><br></pre></td></tr></table></figure><hr><h1 id="14-RDMA-WRITE-的完整流程"><a href="#14-RDMA-WRITE-的完整流程" class="headerlink" title="14. RDMA WRITE 的完整流程"></a>14. RDMA WRITE 的完整流程</h1><p>现在把 QP 加进来。</p><p>假设：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">A = Initiator</span><br><span class="line">B = Target</span><br></pre></td></tr></table></figure><hr><h2 id="Step-0：两边先建立-QP"><a href="#Step-0：两边先建立-QP" class="headerlink" title="Step 0：两边先建立 QP"></a>Step 0：两边先建立 QP</h2><p>两边都创建：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">QP_A</span><br><span class="line">QP_B</span><br></pre></td></tr></table></figure><p>对于 RC：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Reliable Connected</span><br></pre></td></tr></table></figure><p>需要建立：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP_A &lt;------------&gt; QP_B</span><br></pre></td></tr></table></figure><p>连接关系。</p><p>例如会交换：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">QPN</span><br><span class="line">PSN</span><br><span class="line">LID / GID</span><br></pre></td></tr></table></figure><p>等连接信息。</p><hr><h2 id="Step-1：B-注册远端内存"><a href="#Step-1：B-注册远端内存" class="headerlink" title="Step 1：B 注册远端内存"></a>Step 1：B 注册远端内存</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">B CPU</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">malloc()</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">Buffer</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">ibv_reg_mr()</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">MR</span><br></pre></td></tr></table></figure><p>得到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">addr = 0x70000000</span><br><span class="line">rkey = 0x2222</span><br></pre></td></tr></table></figure><hr><h2 id="Step-2：B-把远端信息告诉-A"><a href="#Step-2：B-把远端信息告诉-A" class="headerlink" title="Step 2：B 把远端信息告诉 A"></a>Step 2：B 把远端信息告诉 A</h2><p>控制面：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">B                         A</span><br><span class="line"></span><br><span class="line">remote_addr</span><br><span class="line">rkey</span><br><span class="line">buffer size</span><br><span class="line">    --------------------&gt;</span><br></pre></td></tr></table></figure><p>至此 A 知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">我要往 B 的哪里写。</span><br></pre></td></tr></table></figure><hr><h1 id="15-Step-3：A-创建并提交-RDMA-WRITE-WR"><a href="#15-Step-3：A-创建并提交-RDMA-WRITE-WR" class="headerlink" title="15. Step 3：A 创建并提交 RDMA WRITE WR"></a>15. Step 3：A 创建并提交 RDMA WRITE WR</h1><p>A 构造：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">WR</span><br><span class="line"></span><br><span class="line">opcode      = RDMA_WRITE</span><br><span class="line"></span><br><span class="line">local_addr</span><br><span class="line">length</span><br><span class="line">lkey</span><br><span class="line"></span><br><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ibv_post_send(qp_A, &amp;wr, ...);</span><br></pre></td></tr></table></figure><p>逻辑上：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">A Application</span><br><span class="line">     │</span><br><span class="line">     │ ibv_post_send()</span><br><span class="line">     ▼</span><br><span class="line">   QP_A.SQ</span><br><span class="line">     │</span><br><span class="line">     │ WQE</span><br><span class="line">     ▼</span><br></pre></td></tr></table></figure><p>WQE 被放进：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP_A 的 Send Queue</span><br></pre></td></tr></table></figure><hr><h1 id="16-Step-4：Doorbell"><a href="#16-Step-4：Doorbell" class="headerlink" title="16. Step 4：Doorbell"></a>16. Step 4：Doorbell</h1><p>仅仅把 WQE 写进 SQ 还不够。</p><p>需要告诉 RNIC：</p><blockquote><p>SQ 的 producer 状态更新了，有新的 WQE 可以执行。</p></blockquote><p>于是 CPU 或 GPU 会：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ring doorbell</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CPU</span><br><span class="line"> │</span><br><span class="line"> │ write doorbell</span><br><span class="line"> ▼</span><br><span class="line">RNIC</span><br></pre></td></tr></table></figure><p>RNIC 于是知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP_A.SQ 中出现了新的任务。</span><br></pre></td></tr></table></figure><hr><h1 id="17-Step-5：A-RNIC-获取-WQE"><a href="#17-Step-5：A-RNIC-获取-WQE" class="headerlink" title="17. Step 5：A RNIC 获取 WQE"></a>17. Step 5：A RNIC 获取 WQE</h1><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">A RNIC</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">读取 QP_A.SQ</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">解析 WQE</span><br></pre></td></tr></table></figure><p>发现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">opcode = RDMA_WRITE</span><br></pre></td></tr></table></figure><p>并解析：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">local_addr</span><br><span class="line">length</span><br><span class="line">lkey</span><br><span class="line"></span><br><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><hr><h1 id="18-Step-6：A-RNIC-DMA-读取本地内存"><a href="#18-Step-6：A-RNIC-DMA-读取本地内存" class="headerlink" title="18. Step 6：A RNIC DMA 读取本地内存"></a>18. Step 6：A RNIC DMA 读取本地内存</h1><p>A RNIC 根据：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">local_addr + lkey</span><br></pre></td></tr></table></figure><p>访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">A Memory</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">A Memory</span><br><span class="line">0x10000000</span><br><span class="line">     │</span><br><span class="line">     │ PCIe DMA READ</span><br><span class="line">     ▼</span><br><span class="line">A RNIC</span><br></pre></td></tr></table></figure><p>也就是说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CPU 不负责 memcpy</span><br></pre></td></tr></table></figure><p>数据由 RNIC DMA 取得。</p><hr><h1 id="19-Step-7：通过网络发给-B"><a href="#19-Step-7：通过网络发给-B" class="headerlink" title="19. Step 7：通过网络发给 B"></a>19. Step 7：通过网络发给 B</h1><p>RNIC 将数据封装成 RDMA 网络包：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">A RNIC</span><br><span class="line">   │</span><br><span class="line">   │ RDMA WRITE packets</span><br><span class="line">   ▼</span><br><span class="line">Network</span><br><span class="line">   │</span><br><span class="line">   ▼</span><br><span class="line">B RNIC</span><br></pre></td></tr></table></figure><p>包里会包含足够的信息，让 B RNIC 找到目标 MR，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">remote virtual address</span><br><span class="line">rkey</span><br><span class="line">payload</span><br></pre></td></tr></table></figure><hr><h1 id="20-Step-8：B-RNIC-验证-rkey"><a href="#20-Step-8：B-RNIC-验证-rkey" class="headerlink" title="20. Step 8：B RNIC 验证 rkey"></a>20. Step 8：B RNIC 验证 rkey</h1><p>B RNIC 收到以后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">B RNIC</span><br><span class="line"> │</span><br><span class="line"> ├── 检查 rkey</span><br><span class="line"> │</span><br><span class="line"> ├── 检查访问权限</span><br><span class="line"> │</span><br><span class="line"> └── 检查地址是否合法</span><br></pre></td></tr></table></figure><p>例如确认：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0x70000000</span><br></pre></td></tr></table></figure><p>确实属于对应 MR。</p><hr><h1 id="21-Step-9：B-RNIC-DMA-写入-B-内存"><a href="#21-Step-9：B-RNIC-DMA-写入-B-内存" class="headerlink" title="21. Step 9：B RNIC DMA 写入 B 内存"></a>21. Step 9：B RNIC DMA 写入 B 内存</h1><p>验证成功：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">B RNIC</span><br><span class="line"> │</span><br><span class="line"> │ PCIe DMA WRITE</span><br><span class="line"> ▼</span><br><span class="line">B Memory</span><br><span class="line"></span><br><span class="line">0x70000000</span><br></pre></td></tr></table></figure><p>于是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">A buffer</span><br><span class="line">0x10000000</span><br><span class="line"></span><br><span class="line">        │</span><br><span class="line">        │ RDMA WRITE</span><br><span class="line">        ▼</span><br><span class="line"></span><br><span class="line">B buffer</span><br><span class="line">0x70000000</span><br></pre></td></tr></table></figure><p>整个数据面过程中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">B CPU 没有调用 recv()</span><br><span class="line">B RQ 没有消费 WQE</span><br></pre></td></tr></table></figure><hr><h1 id="22-Step-10：完成通知"><a href="#22-Step-10：完成通知" class="headerlink" title="22. Step 10：完成通知"></a>22. Step 10：完成通知</h1><p>对于可靠连接 RC，远端 RNIC 会按照协议完成可靠传输确认。</p><p>最终 A RNIC 可以在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">A CQ</span><br></pre></td></tr></table></figure><p>中生成一个 CQE。</p><p>然后 A：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ibv_poll_cq(...)</span><br></pre></td></tr></table></figure><p>得到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Work Completion</span><br></pre></td></tr></table></figure><p>于是知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这条 RDMA WRITE 已经完成。</span><br></pre></td></tr></table></figure><p>完整路径：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">              Host A                              Host B</span><br><span class="line"></span><br><span class="line">Application</span><br><span class="line">    │</span><br><span class="line">    │ post_send</span><br><span class="line">    ▼</span><br><span class="line"> QP_A.SQ</span><br><span class="line">    │</span><br><span class="line">    │ WQE</span><br><span class="line">    ▼</span><br><span class="line"> RNIC A</span><br><span class="line">    │</span><br><span class="line">    │ DMA READ</span><br><span class="line">    ▼</span><br><span class="line">A Memory</span><br><span class="line">    │</span><br><span class="line">    │</span><br><span class="line">    └────────────┐</span><br><span class="line">                 │</span><br><span class="line">                 ▼</span><br><span class="line">              RNIC A</span><br><span class="line">                 │</span><br><span class="line">                 │ RDMA WRITE</span><br><span class="line">                 │</span><br><span class="line">=================│================================ Network</span><br><span class="line">                 │</span><br><span class="line">                 ▼</span><br><span class="line">              RNIC B</span><br><span class="line">                 │</span><br><span class="line">                 │ check rkey</span><br><span class="line">                 │ DMA WRITE</span><br><span class="line">                 ▼</span><br><span class="line">              B Memory</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">RNIC A</span><br><span class="line">   │</span><br><span class="line">   ▼</span><br><span class="line">A CQ</span><br><span class="line">   │</span><br><span class="line">   ▼</span><br><span class="line">Work Completion</span><br></pre></td></tr></table></figure><p>注意：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B.RQ 完全没有出现在数据路径中。</span><br></pre></td></tr></table></figure><hr><h1 id="23-那么-PUT-是什么？"><a href="#23-那么-PUT-是什么？" class="headerlink" title="23. 那么 PUT 是什么？"></a>23. 那么 PUT 是什么？</h1><p>这里需要特别区分。</p><p>在标准 InfiniBand Verbs 中，通常使用的术语是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IBV_WR_RDMA_WRITE</span><br></pre></td></tr></table></figure><p>也就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE</span><br></pre></td></tr></table></figure><p><strong>Verbs 本身并没有一个独立的 <code>IBV_WR_PUT</code> 操作。</strong></p><p>但是很多上层通信模型中会使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br></pre></td></tr></table></figure><p>这个词。</p><p>PUT 的语义通常是：</p><blockquote><p>把本地数据放到远端指定地址。</p></blockquote><p>也就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT(local_buffer, remote_buffer)</span><br></pre></td></tr></table></figure><p>从语义来看：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT ≈ remote write</span><br></pre></td></tr></table></figure><p>因此很多 RDMA 系统中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br><span class="line">    ↓</span><br><span class="line">最终底层实现</span><br><span class="line">    ↓</span><br><span class="line">RDMA WRITE</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Application</span><br><span class="line"></span><br><span class="line">put(dst_remote, src_local, size)</span><br><span class="line"></span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line"></span><br><span class="line">Runtime / Communication Library</span><br><span class="line"></span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line"></span><br><span class="line">ibv_post_send()</span><br><span class="line"></span><br><span class="line">opcode = IBV_WR_RDMA_WRITE</span><br><span class="line"></span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line"></span><br><span class="line">QP.SQ</span><br></pre></td></tr></table></figure><p>所以如果讨论的是普通 RDMA&#x2F;InfiniBand：</p><blockquote><p>PUT 通常不是另一种硬件传输协议，而是上层 API 对”把数据写到远端”这一动作的称呼，底层通常映射为 RDMA WRITE。</p></blockquote><hr><h1 id="24-RDMA-WRITE-和-PUT-的-Step-3-分别是什么？"><a href="#24-RDMA-WRITE-和-PUT-的-Step-3-分别是什么？" class="headerlink" title="24. RDMA WRITE 和 PUT 的 Step 3 分别是什么？"></a>24. RDMA WRITE 和 PUT 的 Step 3 分别是什么？</h1><p>如果老师把：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WRITE</span><br></pre></td></tr></table></figure><p>和：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br></pre></td></tr></table></figure><p>分开讲，通常可以这样理解。</p><hr><h2 id="RDMA-WRITE-的-Step-3"><a href="#RDMA-WRITE-的-Step-3" class="headerlink" title="RDMA WRITE 的 Step 3"></a>RDMA WRITE 的 Step 3</h2><p>用户直接构造 RDMA WRITE WR：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Application</span><br><span class="line">    │</span><br><span class="line">    │ create WR</span><br><span class="line">    ▼</span><br><span class="line"></span><br><span class="line">opcode = RDMA_WRITE</span><br><span class="line"></span><br><span class="line">local_addr</span><br><span class="line">length</span><br><span class="line">lkey</span><br><span class="line"></span><br><span class="line">remote_addr</span><br><span class="line">rkey</span><br><span class="line"></span><br><span class="line">    │</span><br><span class="line">    │ post_send</span><br><span class="line">    ▼</span><br><span class="line"></span><br><span class="line">QP.SQ</span><br></pre></td></tr></table></figure><p>即：</p><blockquote><p>发起方直接把一个 <code>RDMA WRITE WQE</code> 放入本地 QP 的 SQ，随后 ring doorbell，RNIC 消费该 WQE。</p></blockquote><hr><h2 id="PUT-的-Step-3"><a href="#PUT-的-Step-3" class="headerlink" title="PUT 的 Step 3"></a>PUT 的 Step 3</h2><p>如果 <code>PUT</code> 是一个上层 API：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">put(remote_addr, local_addr, size)</span><br></pre></td></tr></table></figure><p>那么 runtime 首先需要：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">查找远端地址对应信息</span><br><span class="line"> │</span><br><span class="line"> ├── remote_addr</span><br><span class="line"> └── rkey</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">构造 RDMA WRITE WR</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line">post 到 QP.SQ</span><br></pre></td></tr></table></figure><p>之后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT 和 RDMA WRITE 的底层流程基本相同。</span><br></pre></td></tr></table></figure><p>也就是说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Application PUT</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">Runtime</span><br><span class="line">      │</span><br><span class="line">      │ translate</span><br><span class="line">      ▼</span><br><span class="line">RDMA WRITE WR</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">QP.SQ</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">RNIC</span><br></pre></td></tr></table></figure><hr><h1 id="25-从-QP-角度比较-WRITE-和-PUT"><a href="#25-从-QP-角度比较-WRITE-和-PUT" class="headerlink" title="25. 从 QP 角度比较 WRITE 和 PUT"></a>25. 从 QP 角度比较 WRITE 和 PUT</h1><p>可以画成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE：</span><br><span class="line"></span><br><span class="line">Application</span><br><span class="line">    │</span><br><span class="line">    │ ibv_post_send(RDMA_WRITE)</span><br><span class="line">    ▼</span><br><span class="line"> QP.SQ</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line"> RNIC</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Remote Memory</span><br></pre></td></tr></table></figure><p>而 PUT：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Application</span><br><span class="line">    │</span><br><span class="line">    │ put()</span><br><span class="line">    ▼</span><br><span class="line">Runtime / Library</span><br><span class="line">    │</span><br><span class="line">    │ convert to RDMA WRITE</span><br><span class="line">    ▼</span><br><span class="line"> QP.SQ</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line"> RNIC</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Remote Memory</span><br></pre></td></tr></table></figure><p>核心区别通常只在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">API / programming abstraction</span><br></pre></td></tr></table></figure><p>而不是网络数据路径。</p><hr><h1 id="26-单边-WRITE-时，两端-QP-分别在干什么？"><a href="#26-单边-WRITE-时，两端-QP-分别在干什么？" class="headerlink" title="26. 单边 WRITE 时，两端 QP 分别在干什么？"></a>26. 单边 WRITE 时，两端 QP 分别在干什么？</h1><p>这个问题值得单独说明。</p><p>假设：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP_A &lt;======== RC connection ========&gt; QP_B</span><br></pre></td></tr></table></figure><p>A 发 WRITE：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">QP_A.SQ</span><br><span class="line">  │</span><br><span class="line">  │ RDMA WRITE WQE</span><br><span class="line">  ▼</span><br><span class="line">RNIC A</span><br></pre></td></tr></table></figure><p>但是 B：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP_B.RQ</span><br></pre></td></tr></table></figure><p>没有 Receive WQE 被消费。</p><p>可以理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">             RC QP Connection</span><br><span class="line"></span><br><span class="line">A QP                                  B QP</span><br><span class="line"></span><br><span class="line">┌───────────────┐                ┌───────────────┐</span><br><span class="line">│               │                │               │</span><br><span class="line">│ SQ            │                │ SQ            │</span><br><span class="line">│               │                │               │</span><br><span class="line">│ WRITE WQE ----│---------------&gt;│ RNIC handling │</span><br><span class="line">│               │                │               │</span><br><span class="line">│ RQ            │                │ RQ            │</span><br><span class="line">│               │                │               │</span><br><span class="line">│               │                │     unused    │</span><br><span class="line">└───────────────┘                └───────────────┘</span><br><span class="line">                                         │</span><br><span class="line">                                         ▼</span><br><span class="line">                                    B Memory</span><br></pre></td></tr></table></figure><p>这里：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B QP</span><br></pre></td></tr></table></figure><p>依然参与 RC 连接的协议状态，例如可靠性、packet sequence 等。</p><p>但：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">B.RQ</span><br></pre></td></tr></table></figure><p>不负责提供目标 buffer。</p><p>目标 buffer 来自：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">remote_addr + rkey</span><br></pre></td></tr></table></figure><hr><h1 id="27-这和-SEND-RECV-最大区别在哪里？"><a href="#27-这和-SEND-RECV-最大区别在哪里？" class="headerlink" title="27. 这和 SEND &#x2F; RECV 最大区别在哪里？"></a>27. 这和 SEND &#x2F; RECV 最大区别在哪里？</h1><h3 id="SEND"><a href="#SEND" class="headerlink" title="SEND"></a>SEND</h3><p>A 只需要知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">我要发送 local buffer</span><br></pre></td></tr></table></figure><p>并不知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">最终写到 B 的哪个具体虚拟地址</span><br></pre></td></tr></table></figure><p>B 必须提前：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">post_recv()</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">A                                 B</span><br><span class="line"></span><br><span class="line">QP_A.SQ                            QP_B.RQ</span><br><span class="line"></span><br><span class="line">SEND WQE -----------------------&gt; RECV WQE</span><br><span class="line">                                     │</span><br><span class="line">                                     ▼</span><br><span class="line">                                  Buffer</span><br></pre></td></tr></table></figure><hr><h3 id="RDMA-WRITE"><a href="#RDMA-WRITE" class="headerlink" title="RDMA WRITE"></a>RDMA WRITE</h3><p>A 明确知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">B.remote_addr</span><br><span class="line">B.rkey</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">A                                 B</span><br><span class="line"></span><br><span class="line">QP_A.SQ</span><br><span class="line"></span><br><span class="line">RDMA WRITE WQE ----------------&gt; registered MR</span><br><span class="line">                                   0x70000000</span><br><span class="line"></span><br><span class="line">QP_B.RQ</span><br><span class="line">   X</span><br></pre></td></tr></table></figure><p>所以二者最大的区别可以理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">SEND：</span><br><span class="line"></span><br><span class="line">&quot;B，你找一块提前准备好的 receive buffer 接住它。&quot;</span><br><span class="line"></span><br><span class="line">RDMA WRITE：</span><br><span class="line"></span><br><span class="line">&quot;B，我已经知道地址了，我直接写到 0x70000000。&quot;</span><br></pre></td></tr></table></figure><hr><h1 id="28-最后总结"><a href="#28-最后总结" class="headerlink" title="28. 最后总结"></a>28. 最后总结</h1><h2 id="什么叫单边-RDMA？"><a href="#什么叫单边-RDMA？" class="headerlink" title="什么叫单边 RDMA？"></a>什么叫单边 RDMA？</h2><p>单边 RDMA 是：</p><blockquote><p>发起方 RNIC 可以直接访问远端已经注册并授权的内存，远端 CPU 不需要参与每一次数据搬运。</p></blockquote><p>典型操作：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">RDMA READ</span><br><span class="line">RDMA WRITE</span><br><span class="line">Atomic</span><br></pre></td></tr></table></figure><hr><h2 id="什么是-QP？"><a href="#什么是-QP？" class="headerlink" title="什么是 QP？"></a>什么是 QP？</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QP = SQ + RQ</span><br></pre></td></tr></table></figure><p>SQ：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">主动发送任务</span><br><span class="line">SEND</span><br><span class="line">READ</span><br><span class="line">WRITE</span><br><span class="line">Atomic</span><br></pre></td></tr></table></figure><p>RQ：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">主要用于接收 SEND</span><br></pre></td></tr></table></figure><p>单边 READ&#x2F;WRITE：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">不需要远端提前 post_recv()</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">远端 RQ 不参与数据搬运。</span><br></pre></td></tr></table></figure><hr><h2 id="一个-RDMA-WRITE-WR-要有什么？"><a href="#一个-RDMA-WRITE-WR-要有什么？" class="headerlink" title="一个 RDMA WRITE WR 要有什么？"></a>一个 RDMA WRITE WR 要有什么？</h2><p>最核心的信息：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">                 RDMA WRITE WR</span><br><span class="line"></span><br><span class="line">       Local                         Remote</span><br><span class="line"></span><br><span class="line">local_addr                       remote_addr</span><br><span class="line">length                           rkey</span><br><span class="line">lkey</span><br><span class="line"></span><br><span class="line">              +</span><br><span class="line">            opcode</span><br><span class="line">            wr_id</span><br><span class="line">          send_flags</span><br></pre></td></tr></table></figure><p>即：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">我要干什么？</span><br><span class="line">WRITE</span><br><span class="line"></span><br><span class="line">从哪里读取？</span><br><span class="line">local_addr + lkey</span><br><span class="line"></span><br><span class="line">读多少？</span><br><span class="line">length</span><br><span class="line"></span><br><span class="line">写到哪里？</span><br><span class="line">remote_addr</span><br><span class="line"></span><br><span class="line">有没有权限？</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><hr><h2 id="接收方要提供什么？"><a href="#接收方要提供什么？" class="headerlink" title="接收方要提供什么？"></a>接收方要提供什么？</h2><p>最核心：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">remote_addr</span><br><span class="line">rkey</span><br></pre></td></tr></table></figure><p>实际系统通常还会告诉：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">buffer capacity</span><br></pre></td></tr></table></figure><p>这些信息一般通过控制面提前交换。</p><hr><h2 id="RDMA-WRITE-完整数据路径"><a href="#RDMA-WRITE-完整数据路径" class="headerlink" title="RDMA WRITE 完整数据路径"></a>RDMA WRITE 完整数据路径</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">A Application</span><br><span class="line">      │</span><br><span class="line">      │ WR</span><br><span class="line">      ▼</span><br><span class="line">A QP.SQ</span><br><span class="line">      │</span><br><span class="line">      │ WQE</span><br><span class="line">      ▼</span><br><span class="line">   Doorbell</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">A RNIC</span><br><span class="line">      │</span><br><span class="line">      │ DMA read</span><br><span class="line">      ▼</span><br><span class="line">A Memory</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">A RNIC</span><br><span class="line">      │</span><br><span class="line">      │ network</span><br><span class="line">      ▼</span><br><span class="line">B RNIC</span><br><span class="line">      │</span><br><span class="line">      │ check rkey</span><br><span class="line">      │ DMA write</span><br><span class="line">      ▼</span><br><span class="line">B Memory</span><br><span class="line"></span><br><span class="line">      B CPU 不参与</span><br><span class="line">      B RQ 不参与</span><br></pre></td></tr></table></figure><hr><h2 id="WRITE-和-PUT"><a href="#WRITE-和-PUT" class="headerlink" title="WRITE 和 PUT"></a>WRITE 和 PUT</h2><p>在 InfiniBand Verbs 中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RDMA WRITE</span><br></pre></td></tr></table></figure><p>是真正的硬件 Work Request 类型。</p><p>而：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br></pre></td></tr></table></figure><p>通常是上层编程模型中的语义：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">PUT</span><br><span class="line"> ↓</span><br><span class="line">runtime</span><br><span class="line"> ↓</span><br><span class="line">RDMA WRITE WR</span><br><span class="line"> ↓</span><br><span class="line">QP.SQ</span><br><span class="line"> ↓</span><br><span class="line">RNIC</span><br><span class="line"> ↓</span><br><span class="line">Remote Memory</span><br></pre></td></tr></table></figure><p>因此大多数情况下：</p><blockquote><p><strong>PUT 是”我要把这段数据放到远端”的 API 语义，而 RDMA WRITE 是底层真正执行这个动作的 RDMA Verb。</strong></p></blockquote>]]>
    </content>
    <id>https://loosp.cn/2026/09/09/rdma-one-sided-qp-wr-wqe/</id>
    <link href="https://loosp.cn/2026/09/09/rdma-one-sided-qp-wr-wqe/"/>
    <published>2026-09-09T13:00:00.000Z</published>
    <summary>从 QP / WR / WQE 讲起，梳理单边 RDMA WRITE 的完整数据路径，并辨析 RDMA WRITE 与 PUT 的关系。</summary>
    <title>RDMA 单边操作：QP、WR/WQE 与 RDMA WRITE / PUT 流程</title>
    <updated>2026-09-08T20:39:20.728Z</updated>
  </entry>
  <entry>
    <author>
      <name>loosp</name>
    </author>
    <category term="复习" scheme="https://loosp.cn/categories/%E5%A4%8D%E4%B9%A0/"/>
    <content>
      <![CDATA[<p>只是占位</p>]]>
    </content>
    <id>https://loosp.cn/2026/09/06/review-placeholder/</id>
    <link href="https://loosp.cn/2026/09/06/review-placeholder/"/>
    <published>2026-09-06T03:00:00.000Z</published>
    <summary>占位文章，防止分类为空；后续可删除或替换为真实的复习笔记。</summary>
    <title>复习（占位）</title>
    <updated>2026-09-08T21:01:13.479Z</updated>
  </entry>
</feed>
