<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>ji的博客</title>
  
  
  <link href="https://liangji-seu.github.io/atom.xml" rel="self"/>
  
  <link href="https://liangji-seu.github.io/"/>
  <updated>2026-10-03T10:34:31.622Z</updated>
  <id>https://liangji-seu.github.io/</id>
  
  <author>
    <name>liangji</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>infra FlashAttention学习</title>
    <link href="https://liangji-seu.github.io/2026/08/25/infra-FlashAttention%E5%AD%A6%E4%B9%A0/"/>
    <id>https://liangji-seu.github.io/2026/08/25/infra-FlashAttention%E5%AD%A6%E4%B9%A0/</id>
    <published>2026-08-25T06:54:57.000Z</published>
    <updated>2026-10-03T10:34:31.622Z</updated>
    
    <content type="html"><![CDATA[<p>这篇是我在学习triton算子的时候，开始正式学习flashattention的原理，发现有很多版本，所以单独写一篇来记录：</p><h1 id="先验知识"><a href="#先验知识" class="headerlink" title="先验知识"></a>先验知识</h1><p>MAC（Memory Access Cost，存储访问开销）</p><p><img src="/images/Pasted%20image%2020260819194222.png"></p><h1 id="朴素注意力算子"><a href="#朴素注意力算子" class="headerlink" title="朴素注意力算子"></a>朴素注意力算子</h1><p>关于注意力计算，一开始我们已经学习了：</p><ul><li>单头注意力self-Attention</li><li>多头注意力MHA</li><li>分组多头注意力GQA</li></ul><p>我们原始的朴素注意力的计算，主要有3个阶段</p><p><img src="/images/Pasted%20image%2020260819194652.png"></p><p>所以，一共包含8次HBM全局显存的读写操作</p><p><img src="/images/Pasted%20image%2020260819194731.png" alt="574"></p><h1 id="v1"><a href="#v1" class="headerlink" title="v1"></a>v1</h1><h2 id="softmax的更新"><a href="#softmax的更新" class="headerlink" title="softmax的更新"></a>softmax的更新</h2><h3 id="选用稳定版的softmatx"><a href="#选用稳定版的softmatx" class="headerlink" title="选用稳定版的softmatx"></a>选用稳定版的softmatx</h3><p><img src="/images/Pasted%20image%2020260819195101.png"></p><h3 id="分块向量中softmax的计算"><a href="#分块向量中softmax的计算" class="headerlink" title="分块向量中softmax的计算"></a>分块向量中softmax的计算</h3><p>x 向量，划分成 x1 + x2向量，不能单独计算各自的softmax(x1), softmax(x2)</p><p>x1向量，计算出m(x1), l(x1)后，仅保存这两个值，就不用保存x1向量的完整向量了</p><ul><li>m(x1)<ul><li>x1向量的最大值</li></ul></li><li>l(x1)<ul><li>分母Σ和</li></ul></li></ul><p><strong>这里x1就处理完了，下面开始处理x2</strong></p><ul><li>求出m(x2)</li><li>求出l(x2)</li></ul><p><strong>之后开始处理更新全局</strong></p><ul><li>m(x) &#x3D; max(m)<ul><li>从m(x1), m(x2)中选一个出来</li></ul></li><li>l(x),更新计算错误的那一个分母<ul><li>乘回去，重新除</li><li><img src="/images/Pasted%20image%2020260819195716.png" alt="389"></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260819195822.png"></p><p><img src="/images/Pasted%20image%2020260819195911.png" alt="449"></p><p><img src="/images/Pasted%20image%2020260819200019.png" alt="482"></p><p>上述其实是一个增量计算的过程。我们首先计算一个分块的局部softmax值，然后存储起来。当处理完下一个分块时，可以根据此时的新的<strong>全局最大值</strong>和<strong>全局EXP求和项</strong>来更新旧的softmax值。接着再处理下一个分块，然后再更新。当处理完所有分块后，此时的所有分块的softmax值都是“全局的”。</p><h2 id="flashattention"><a href="#flashattention" class="headerlink" title="flashattention"></a>flashattention</h2><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">以下是我的理解：</span><br><span class="line"></span><br><span class="line">我已经彻底理解了，未归一化的累加器，就是分块矩阵乘分数到V上的，就单单乘分数的分子，分数更新，由于每次都是只乘分子，所以直接把这个更新的alpha乘到旧的O_i就可以完成更新，最后统一除一个最新的全的l分母，至此，完成，本质上这就是一个把softmax的计算，拆分结合到分块矩阵乘里面，利用仅分子乘的乘法结合律的特性，每步更新历史的分数状态，因为是每步更新，所以历史分数里面，都始终是统一的上一轮更新上去的新分数，只要乘一个alpha就可以完成整体历史分数的修正，在O_i上，然后加上分块矩阵的乘法的当前一个分块，所以O_i里面的内容，应该这样看，是Σ（每个循环步的分数_该循环步对应的v_k）， 因为每个循环步的分数都是累计更新的，第一次只有j=0的分数，第二次j=1来了，j=0的分数乘上alpha，然后j=0的分数_v_0 加上这一次的j=1的分数_v_1, 然后j=2来了，j=0,1的分数都再次乘上alpha进行修正，然后加上j=2的分数_v_2, 这样一直累计更新。直到最后一步，O_i 直接除l_i， 这个一直累计的分母，得到我们真正的O_i</span><br></pre></td></tr></table></figure><p>下面画个图解释一下<br><img src="/images/Pasted%20image%2020260819234259.png"></p><p>那我们vllm里面的flashattention相较于朴素实现的flashattention有哪些结合的优化呢：</p><h3 id="pagedattention优化"><a href="#pagedattention优化" class="headerlink" title="pagedattention优化"></a>pagedattention优化</h3><p>朴素实现的标准flashattention, 输入都是直接一整块的张量显存，但是我们的kvcache张量线程，都已经是用block来统计的。</p><p>所以我们的flashattention传入的应该是slot_mappings，读写那个token的kvcache向量，就直接通过slot_mapping来跳转查找</p><h3 id="变长batch"><a href="#变长batch" class="headerlink" title="变长batch"></a>变长batch</h3><p>支持变长batching，朴素的batch发过来是，填充后的<br>req_0: 1,2,3, xx, xx, xx, xx<br>req_1: 43, 23,34,12, xx,xx<br>req_2:1, xx,xx,xx,xx,xx<br>这样每个req都是等长，然后内部挨个用mask来屏蔽计算的。</p><p>但是我们vllm有支持变长batch，所以发过来的就是input_ids的那样<br>[1, 2, 3, 43, 23, 34, 12, 1]<br>这样，然后通过各种索引映射表来区分那个是req0, 那个是req1, 哪个是req2</p><hr><h3 id="kernel一次处理的多少req的问题"><a href="#kernel一次处理的多少req的问题" class="headerlink" title="kernel一次处理的多少req的问题"></a>kernel一次处理的多少req的问题</h3><p>我们的<br><img src="/images/Pasted%20image%2020260820002016.png"></p><h2 id="my-vllm里面的Qwen2Attention层实现"><a href="#my-vllm里面的Qwen2Attention层实现" class="headerlink" title="my-vllm里面的Qwen2Attention层实现"></a>my-vllm里面的Qwen2Attention层实现</h2><p>我们的Decode block里面，Qwen2DecoderLayer类里面主要分为两部分：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Qwen2DecoderLayer</span>:</span><br><span class="line"><span class="variable language_">self</span>.self_attn = Qwen2Attention(config)</span><br><span class="line"><span class="variable language_">self</span>.mlp = Qwen2MLP(config)</span><br><span class="line"><span class="variable language_">self</span>.input_layernorm = RMSNorm(...)</span><br><span class="line"><span class="variable language_">self</span>.post_attention_layernorm = RMSNorm(...)</span><br></pre></td></tr></table></figure><p>可以看到，Qwen2Model的一个layer的decode block里面，主要分成前面的注意力计算，以及后面的FFN的计算。</p><p>所以他是吧qkv投影放到注意力算子层里面一起实现的。</p><blockquote><p>我们kuipa里面是用的matmul + gqa算子两层封装来凑起来的，vllm里面就一层封装</p></blockquote><p>下面我们来看一下Qwen2Attention</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Qwen2Attention</span>:</span><br><span class="line"></span><br><span class="line"><span class="comment"># 投影矩阵</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">Q向量的维度， 可以与K/V向量的维度不一样， 但是K/V向量的维度一般相同</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="variable language_">self</span>.q_proj = nn.Linear(hidden_size, q_size, bias)</span><br><span class="line"><span class="variable language_">self</span>.k_proj = nn.Linear(hidden_size, kv_size, bias)</span><br><span class="line"><span class="variable language_">self</span>.v_proj = nn.Linear(hidden_size, kv_size, bias)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 旋转位置编码</span></span><br><span class="line"><span class="variable language_">self</span>.rope_theta = config.rope_thera</span><br><span class="line"></span><br><span class="line"><span class="comment"># 多头注意力GQA的参数</span></span><br><span class="line"><span class="variable language_">self</span>.num_heads = config.num_attention_heads</span><br><span class="line"><span class="variable language_">self</span>.num_kv_heads = config.num_key_value_heads</span><br><span class="line"><span class="variable language_">self</span>.head_dim = config.hidden_size // config.num_attention_heads</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出投影矩阵，投影回语义空间</span></span><br><span class="line"><span class="variable language_">self</span>.o_proj = nn.Linear(q_size, hidden_size, bias)</span><br><span class="line"></span><br><span class="line"><span class="comment"># kvcache</span></span><br><span class="line"><span class="variable language_">self</span>.kv_cache: torch.Tensor</span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">注意，GQA里面，每个q_i头，都得到一个头的v_q_i,（这个v_q_i是通过和k头算权重，在v头上加权得到的，每个v_q_i都是一个head_dim向量）</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">所以gqa的注意力输出，在进入O投影之前，就是和Q向量一样的维度。是q_size</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260820221544.png" alt="493"></p><p>注意，每个注意力层实例，<mark style="background:#fff88f">里面有一个关键的self.kv_cache : torch.Tensor</mark></p><p><mark style="background:#fff88f">这个就是model_runner在初始化kvcache后，划分出每层的tensor，然后reshape后，这个张量实例，就是保存到模型的每个注意力层的实例里面了</mark></p><p><img src="/images/Pasted%20image%2020260820223529.png" alt="513"></p><p>看一下前向推理，可以看到，我们这个注意力层的输入，不是向量，是一个张量，意味着这是<mark style="background:#fff88f">可以批量处理的</mark>， 一口气处理一整个batch的token向量</p><p>具体实现是，里面每个投影矩阵本身是nn.Linear，支持张量输入，批量处理，然后计算结果也是张量，</p><blockquote><p><code>torch.Tensor.view()</code> 是在不改变底层数据的前提下，重新解释tensor的形状</p></blockquote><blockquote><p>view 和 reshape的区别：<br>view() 必须尽量共享原来的底层存储<br>reshape()更灵活，必要时可以偷偷复制一份数据</p></blockquote><h1 id="v2"><a href="#v2" class="headerlink" title="v2"></a>v2</h1><h1 id="v3"><a href="#v3" class="headerlink" title="v3"></a>v3</h1><h1 id="v4"><a href="#v4" class="headerlink" title="v4"></a>v4</h1>]]></content>
    
    
    <summary type="html">从朴素注意力与 online softmax 出发，记录 FlashAttention、PagedAttention 及 vLLM 注意力实现的学习过程。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="算子与性能" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E7%AE%97%E5%AD%90%E4%B8%8E%E6%80%A7%E8%83%BD/"/>
    
    
    <category term="FlashAttention" scheme="https://liangji-seu.github.io/tags/FlashAttention/"/>
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="vLLM" scheme="https://liangji-seu.github.io/tags/vLLM/"/>
    
    <category term="Attention" scheme="https://liangji-seu.github.io/tags/Attention/"/>
    
  </entry>
  
  <entry>
    <title>infra 流畅的python 学习</title>
    <link href="https://liangji-seu.github.io/2026/08/11/infra-%E6%B5%81%E7%95%85%E7%9A%84python-%E5%AD%A6%E4%B9%A0/"/>
    <id>https://liangji-seu.github.io/2026/08/11/infra-%E6%B5%81%E7%95%85%E7%9A%84python-%E5%AD%A6%E4%B9%A0/</id>
    <published>2026-08-11T11:35:38.000Z</published>
    <updated>2026-10-03T10:34:31.627Z</updated>
    
    <content type="html"><![CDATA[<p>这篇笔记主要是因为在学习vllm的过程中遇到很多python的高级用法，在这里通过阅读 fluent python 这本书来学习这些高级的用法，对python这门语言更好的理解。</p>]]></content>
    
    
    <summary type="html">从《流畅的 Python》起步，补充 vLLM 学习中遇到的 Python 高级用法；当前仍在起步中。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="工程基础" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%B7%A5%E7%A8%8B%E5%9F%BA%E7%A1%80/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="vLLM" scheme="https://liangji-seu.github.io/tags/vLLM/"/>
    
    <category term="Python" scheme="https://liangji-seu.github.io/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>infra 基于vllm的my-vllm大模型推理框架二次优化</title>
    <link href="https://liangji-seu.github.io/2026/08/11/infra-%E5%9F%BA%E4%BA%8Evllm%E7%9A%84my-vllm%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E6%A1%86%E6%9E%B6%E4%BA%8C%E6%AC%A1%E4%BC%98%E5%8C%96/"/>
    <id>https://liangji-seu.github.io/2026/08/11/infra-%E5%9F%BA%E4%BA%8Evllm%E7%9A%84my-vllm%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E6%A1%86%E6%9E%B6%E4%BA%8C%E6%AC%A1%E4%BC%98%E5%8C%96/</id>
    <published>2026-08-11T11:35:38.000Z</published>
    <updated>2026-10-02T16:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>本项目主要基于对vllm的源码的理解，来实现一个AsyncLLM + EngineCore的 my-vllm项目</p><h1 id="概念扫盲"><a href="#概念扫盲" class="headerlink" title="概念扫盲"></a>概念扫盲</h1><p><mark style="background:#d3f8b6">1. 离线推理&#x2F;在线推理</mark></p><ul><li>离线推理：<ul><li>直接把prompt写在python程序里面启动</li></ul></li><li>在线推理：<ul><li>做成server，接受客户端的访问</li></ul></li></ul><p><mark style="background:#d3f8b6">2. 安装vllm, 查看版本</mark><br><img src="/images/Pasted%20image%2020260728192125.png" alt="172"><br><img src="/images/Pasted%20image%2020260728192137.png"></p><p><mark style="background:#d3f8b6">3. 服务器双进程架构</mark></p><ul><li>进程0：<strong>LLMEngine</strong><ul><li>专注于server功能提供</li></ul></li><li>进程1：<strong>EngineCore</strong><ul><li>专注于模型推理</li></ul></li></ul><figure class="highlight txt"><table><tr><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">            v</span><br><span class="line">+-------------------------+</span><br><span class="line">|        Process 0         |</span><br><span class="line">|       LLMEngine          |</span><br><span class="line">|                          |</span><br><span class="line">|  1. 接收 prompts          |</span><br><span class="line">|  2. tokenizer 分词        |</span><br><span class="line">|  3. 创建请求 Request      |</span><br><span class="line">|                          |</span><br><span class="line">|  EngineCoreClient         |</span><br><span class="line">+------------+-------------+</span><br><span class="line">             |</span><br><span class="line">             |</span><br><span class="line">      ZeroMQ (ZMQ)</span><br><span class="line">      IPC通信通道</span><br><span class="line">             |</span><br><span class="line">             |</span><br><span class="line">             v</span><br><span class="line">+------------+-------------+</span><br><span class="line">|        Process 1         |</span><br><span class="line">|      EngineCore          |</span><br><span class="line">|                          |</span><br><span class="line">|  输入队列 Input Queue     |</span><br><span class="line">|            |             |</span><br><span class="line">|            v             |</span><br><span class="line">|       Scheduler          |</span><br><span class="line">|            |             |</span><br><span class="line">|            v             |</span><br><span class="line">|     调度 GPU 推理         |</span><br><span class="line">|                          |</span><br><span class="line">|  ModelRunner              |</span><br><span class="line">|      |                   |</span><br><span class="line">|      v                   |</span><br><span class="line">|     CUDA                  |</span><br><span class="line">|      |                   |</span><br><span class="line">|      v                   |</span><br><span class="line">|   LLM Model              |</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">      输出队列 Output Queue</span><br><span class="line">             |</span><br><span class="line">             |</span><br><span class="line">      ZeroMQ (ZMQ)</span><br><span class="line">             |</span><br><span class="line">             |</span><br><span class="line">             v</span><br><span class="line">+------------+-------------+</span><br><span class="line">|        Process 0         |</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">             v</span><br><span class="line">        生成文本</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260728192930.png"></p><p><img src="/images/Pasted%20image%2020260728193031.png"></p><p><mark style="background:#d3f8b6">3. RPC，远程过程调用</mark><br>对函数的调用，实际是调用的远程的方法（利用ZMQ等进程间通信实现），但是对上层是无感的，实际上就是把远程服务包装成函数，来实现无感的远程计算</p><p><img src="/images/Pasted%20image%2020260728193926.png" alt="632"></p><ol start="4"><li>vllm的版本<br><img src="/images/Pasted%20image%2020260728200401.png"></li></ol><p><img src="/images/Pasted%20image%2020260728200351.png" alt="464"><img src="/images/Pasted%20image%2020260728200457.png"></p><p><mark style="background:#d3f8b6">4. 协程，async</mark><br>可以简单理解为asyncio这个库带来的一个后台执行任务的机制。<br><img src="/images/Pasted%20image%2020260728212634.png" alt="552"></p><h1 id="vllm和cuda自制推理框架的区别"><a href="#vllm和cuda自制推理框架的区别" class="headerlink" title="vllm和cuda自制推理框架的区别"></a>vllm和cuda自制推理框架的区别</h1><p>我们原来cuda自制推理框架，实现的是最底层的架构。专注于，张量，算子的实现。专注于底层的：</p><ul><li>内存分配器层</li><li>buffer层</li><li>tensor层</li><li>op层</li><li>model层</li><li>最后简单的离线固定demo层。</li></ul><p>而vllm则侧重于针对demo层，把这个推理框架彻底做成一个可以部署使用的服务，而不再是一个简单的demo。</p><p>所以demo继续细分成：</p><ul><li>LLMEngine&#x2F;AsyncLLM<ul><li>用来实现调度，作为服务的前端，来实现一个server</li></ul></li><li>EngineCore<ul><li>就是demo的核心实现，generate()方法，实现一个token的输出</li></ul></li></ul><p><img src="/images/img_v3_02145_56ca00f8-f4e0-44e2-8a71-9ea86be18e5g.jpg"></p><h1 id="ZeroMQ（ZMQ）"><a href="#ZeroMQ（ZMQ）" class="headerlink" title="ZeroMQ（ZMQ）"></a>ZeroMQ（ZMQ）</h1><p>这是一个python的库，用来进行进程间通信的。在这里，我们将先熟悉LLMEngine&#x2F;AsyncLLM 和 EngineCore之间的通信技术</p><p>我们先来了解一下，网络传输模型层级<br><img src="/images/Pasted%20image%2020260801132451.png"><br><img src="/images/Pasted%20image%2020260801132555.png" alt="492"></p><p><mark style="background:#ff4d4f">那socket是什么呢？</mark></p><p>本质上，socket就是一个内核的文件描述符，是通过特定的方式（系统调用）创建出来的内核的内存区域的文件描述符。</p><p><img src="/images/Pasted%20image%2020260801132711.png"></p><p><img src="/images/Pasted%20image%2020260801132918.png"></p><p>而在zmq中，他为了支持N对N的高性能消息通信，对socket的tcp的这种点对点的逻辑，进行了改造，自己内部实现了一个更加抽象的socket<br><img src="/images/Pasted%20image%2020260801133029.png" alt="594"></p><p><img src="/images/Pasted%20image%2020260801133104.png"></p><h2 id="同步REQ-REP通信模式"><a href="#同步REQ-REP通信模式" class="headerlink" title="同步REQ&#x2F;REP通信模式"></a>同步REQ&#x2F;REP通信模式</h2><p><strong>下面实现一个最简单的zmq的server + client</strong><br><img src="/images/Pasted%20image%2020260801134001.png" alt="259"><img src="/images/Pasted%20image%2020260801134016.png" alt="253"></p><p><img src="/images/Pasted%20image%2020260801133948.png"></p><p><mark style="background:#ff4d4f">这种模式要求通信双方严格遵循<strong>一问一答</strong>的交互流程——客户端必须先发送请求并等待回复，不能连续发送两次请求，否则会引发异常</mark></p><p><strong>一个端口只能被一个server进程 bind</strong>，服务器 bind 端口，相当于<strong>我在 5555 号开门营业</strong>：</p><p>尽管多个客户端可以同时连接到该服务端，ZeroMQ 会在传输层缓存它们的请求，但<mark style="background:#fff88f">服务端仍以串行方式逐个处理这些请求</mark>。另外，<mark style="background:#fff88f">服务端无法主动向客户端发起连接</mark>，因此这种模式<mark style="background:#ff4d4f">不符合</mark>我们在 vLLM 中的通信需求</p><h2 id="DEALER-ROUTER通信模式"><a href="#DEALER-ROUTER通信模式" class="headerlink" title="DEALER&#x2F;ROUTER通信模式"></a>DEALER&#x2F;ROUTER通信模式</h2><p>前面的req-rep通信模式的弊端：</p><ul><li>同步阻塞</li><li>串行通信</li></ul><p>这个都不符合我们vllm的需求：</p><ul><li>异步通信</li><li>并发通信</li></ul><p><img src="/images/Pasted%20image%2020260801135330.png"></p><blockquote><p>注意，这里的DEALER套接字实现的异步，是说的send之后，不需要阻塞recv了，可以非阻塞方式recv</p></blockquote><p>所以ROUTER，DEALER都是套接字，可以理解为内核的一块内存区域，类对象，多对多异步双向通信</p><p>启动一个server线程，三个client线程<br><img src="/images/Pasted%20image%2020260801154433.png" alt="365"></p><p><img src="/images/Pasted%20image%2020260801154445.png" alt="475"></p><p>可以看到这边都是可以正常接收的，但是这里有一个问题，就是，我们的服务器，设置了一个ROUTER的socket套接字，这个套接字代表的端口，是用来处理某一类事务的：</p><ul><li><strong>A ROUTER 套接字</strong>：<ul><li>接收推理请求</li></ul></li><li><strong>B ROUTER 套接字</strong>：<ul><li>接收控制命令</li></ul></li><li><strong>C ROUTER 套接字</strong>：<ul><li>订阅配置更新</li></ul></li></ul><p>显然，我们这里定义的一个frontend套接字，是为了推理请求的。然后针对这个frontend，来阻塞接收，用recv_multipart()，这个套接字，会有多个client来进行访问。</p><p><mark style="background:#ff4d4f">这里纠正一个理解</mark>：<br><img src="/images/Pasted%20image%2020260803164317.png"><br>这个其实就是一个一对多的发送机制，不需要遵循应答限制了，直接用就ok</p><p><img src="/images/Pasted%20image%2020260801154531.png" alt="517"></p><p>但是这里有一个问题，当我们的server线程要提供多个功能的时候，每个功能都有一个ROUTER的套接字，这个时候，显然不可能在主循环里面，挨个阻塞，这肯定不行。</p><p>这个时候，就需要<strong>poll机制，来实现批量ROUTER监听</strong>。同时，poll机制还提供超时返回的功能。</p><blockquote><p>我这里还有一个疑问，这里的poll实现的多路监听，本质上是针对server线程的多个ROUTER的套接字，这个每个ROUTER套接字都对应一个端口，代表一个server的接口服务，对吧，但是每个ROUTER是接收多个client的连接的，但是每个端口里面这部还是串行响应client的请求吗？</p></blockquote><p><img src="/images/Pasted%20image%2020260801155746.png"></p><p><img src="/images/Pasted%20image%2020260801155807.png"></p><p><img src="/images/Pasted%20image%2020260801155819.png" alt="402"></p><p><img src="/images/Pasted%20image%2020260801155828.png" alt="436"></p><p>所以，最终应该是poll + 线程池，来实现一个<strong>多路监听，并行处理</strong>的server</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> threading</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">import</span> zmq</span><br><span class="line"><span class="keyword">import</span> argparse</span><br><span class="line"><span class="keyword">import</span> uuid</span><br><span class="line"><span class="keyword">from</span> concurrent.futures <span class="keyword">import</span> ThreadPoolExecutor</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 class="keyword">def</span> <span class="title function_">server_poll_with_workers</span>():</span><br><span class="line">    context = zmq.Context.instance()</span><br><span class="line">    frontend = context.socket(zmq.ROUTER) <span class="comment">#多对一并行接收的套接字</span></span><br><span class="line">    frontend.bind(<span class="string">&quot;tcp://localhost:6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;server start: 6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义模拟推理处理数据的过程</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">process_inference</span>(<span class="params">data</span>):</span><br><span class="line">        time.sleep(<span class="number">0.5</span>)</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">            <span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">            <span class="string">&quot;result&quot;</span>: <span class="string">f&quot;服务器收到消息:<span class="subst">&#123;data[<span class="string">&#x27;query&#x27;</span>]&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;timestamp&quot;</span>: time.time(),</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    poller = zmq.Poller()</span><br><span class="line">    poller.register(frontend, zmq.POLLIN)</span><br><span class="line"></span><br><span class="line">    executor = ThreadPoolExecutor(max_workers=<span class="number">4</span>) <span class="comment">#4个线程的线程池</span></span><br><span class="line">    lock = threading.Lock()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 线程池的workers的工作函数</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">handle_request</span>(<span class="params">identity, request</span>):</span><br><span class="line">        result = process_inference(request)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 构造回复response json</span></span><br><span class="line">        response = &#123;</span><br><span class="line">            <span class="string">&quot;msg_id&quot;</span>: request[<span class="string">&#x27;msg_id&#x27;</span>],</span><br><span class="line">            <span class="string">&quot;reply&quot;</span>: result,</span><br><span class="line">            <span class="string">&quot;from_engine&quot;</span>: <span class="string">&quot;server-0&quot;</span>,</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">with</span> lock:</span><br><span class="line">            <span class="comment"># 走端口返回，因为是4个worker来访问frontend这个套接字，所以需要加锁</span></span><br><span class="line">            frontend.send_multipart([identity, zmq.utils.jsonapi.dumps(response)])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">while</span> <span class="number">1</span>:</span><br><span class="line">            socks = <span class="built_in">dict</span>(poller.poll(<span class="number">1000</span>))</span><br><span class="line">            multipart = []</span><br><span class="line">            <span class="keyword">if</span> frontend <span class="keyword">in</span> socks:</span><br><span class="line">                multipart = frontend.recv_multipart()</span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> multipart:</span><br><span class="line">                    <span class="keyword">continue</span></span><br><span class="line"></span><br><span class="line">                identity = multipart[<span class="number">0</span>]</span><br><span class="line">                message = multipart[-<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">                request = zmq.utils.jsonapi.loads(message)</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">f&quot;server recv: <span class="subst">&#123;identity.decode()&#125;</span> request:<span class="subst">&#123;request[<span class="string">&#x27;msg_id&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">                <span class="comment"># 现在改成向线程池提交异步任务，4个worker并行处理</span></span><br><span class="line">                executor.submit(handle_request, identity, request)</span><br><span class="line"></span><br><span class="line">                <span class="string">&#x27;&#x27;&#x27; 原来的串行处理</span></span><br><span class="line"><span class="string">                result = process_inference(request)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">                # 构造回复response json</span></span><br><span class="line"><span class="string">                response = &#123;</span></span><br><span class="line"><span class="string">                    &quot;msg_id&quot;: request[&#x27;msg_id&#x27;],</span></span><br><span class="line"><span class="string">                    &quot;reply&quot;: result,</span></span><br><span class="line"><span class="string">                    &quot;from_engine&quot;: &quot;server-0&quot;,</span></span><br><span class="line"><span class="string">                &#125;</span></span><br><span class="line"><span class="string">                frontend.send_multipart([identity, zmq.utils.jsonapi.dumps(response)])</span></span><br><span class="line"><span class="string">                &#x27;&#x27;&#x27;</span></span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;server close&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        frontend.close()</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 class="keyword">def</span> <span class="title function_">server_poll</span>():</span><br><span class="line">    context = zmq.Context.instance()</span><br><span class="line">    frontend = context.socket(zmq.ROUTER) <span class="comment">#多对一并行接收的套接字</span></span><br><span class="line">    frontend.bind(<span class="string">&quot;tcp://localhost:6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;server start: 6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义模拟推理处理数据的过程</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">process_inference</span>(<span class="params">data</span>):</span><br><span class="line">        time.sleep(<span class="number">0.5</span>)</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">            <span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">            <span class="string">&quot;result&quot;</span>: <span class="string">f&quot;服务器收到消息:<span class="subst">&#123;data[<span class="string">&#x27;query&#x27;</span>]&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;timestamp&quot;</span>: time.time(),</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    poller = zmq.Poller()</span><br><span class="line">    poller.register(frontend, zmq.POLLIN)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">while</span> <span class="number">1</span>:</span><br><span class="line">            socks = <span class="built_in">dict</span>(poller.poll(<span class="number">1000</span>))</span><br><span class="line">            multipart = []</span><br><span class="line">            <span class="keyword">if</span> frontend <span class="keyword">in</span> socks:</span><br><span class="line">                multipart = frontend.recv_multipart()</span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> multipart:</span><br><span class="line">                    <span class="keyword">continue</span></span><br><span class="line"></span><br><span class="line">                identity = multipart[<span class="number">0</span>]</span><br><span class="line">                message = multipart[-<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">                request = zmq.utils.jsonapi.loads(message)</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">f&quot;server recv: <span class="subst">&#123;identity.decode()&#125;</span> request:<span class="subst">&#123;request[<span class="string">&#x27;msg_id&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">                result = process_inference(request)</span><br><span class="line"></span><br><span class="line">                <span class="comment"># 构造回复response json</span></span><br><span class="line">                response = &#123;</span><br><span class="line">                    <span class="string">&quot;msg_id&quot;</span>: request[<span class="string">&#x27;msg_id&#x27;</span>],</span><br><span class="line">                    <span class="string">&quot;reply&quot;</span>: result,</span><br><span class="line">                    <span class="string">&quot;from_engine&quot;</span>: <span class="string">&quot;server-0&quot;</span>,</span><br><span class="line">                &#125;</span><br><span class="line">                frontend.send_multipart([identity, zmq.utils.jsonapi.dumps(response)])</span><br><span class="line"></span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;server close&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        frontend.close()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">server</span>():</span><br><span class="line">    context = zmq.Context.instance()</span><br><span class="line">    frontend = context.socket(zmq.ROUTER) <span class="comment">#多对一并行接收的套接字</span></span><br><span class="line">    frontend.bind(<span class="string">&quot;tcp://localhost:6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;server start: 6666&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义模拟推理处理数据的过程</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">process_inference</span>(<span class="params">data</span>):</span><br><span class="line">        time.sleep(<span class="number">0.5</span>)</span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">            <span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>,</span><br><span class="line">            <span class="string">&quot;result&quot;</span>: <span class="string">f&quot;服务器收到消息:<span class="subst">&#123;data[<span class="string">&#x27;query&#x27;</span>]&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;timestamp&quot;</span>: time.time(),</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">while</span> <span class="number">1</span>:</span><br><span class="line">            multipart = frontend.recv_multipart()</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> multipart:</span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line"></span><br><span class="line">            identity = multipart[<span class="number">0</span>]</span><br><span class="line">            message = multipart[-<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">            request = zmq.utils.jsonapi.loads(message)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;server recv: <span class="subst">&#123;identity.decode()&#125;</span> request:<span class="subst">&#123;request[<span class="string">&#x27;msg_id&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">            result = process_inference(request)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 构造回复response json</span></span><br><span class="line">            response = &#123;</span><br><span class="line">                <span class="string">&quot;msg_id&quot;</span>: request[<span class="string">&#x27;msg_id&#x27;</span>],</span><br><span class="line">                <span class="string">&quot;reply&quot;</span>: result,</span><br><span class="line">                <span class="string">&quot;from_engine&quot;</span>: <span class="string">&quot;server-0&quot;</span>,</span><br><span class="line">            &#125;</span><br><span class="line">            frontend.send_multipart([identity, zmq.utils.jsonapi.dumps(response)])</span><br><span class="line"></span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;server close&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        frontend.close()</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 class="keyword">def</span> <span class="title function_">client</span>(<span class="params">client_id</span>):</span><br><span class="line">    context = zmq.Context.instance()</span><br><span class="line">    socket = context.socket(zmq.DEALER) <span class="comment"># 异步双向套接字</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">#id编码成字节流</span></span><br><span class="line">    identity = <span class="string">f&quot;Client-<span class="subst">&#123;client_id&#125;</span>&quot;</span>.encode(<span class="string">&quot;utf-8&quot;</span>)</span><br><span class="line">    socket.setsockopt(zmq.IDENTITY, identity)</span><br><span class="line"></span><br><span class="line">    socket.connect(<span class="string">&quot;tcp://localhost:6666&quot;</span>) <span class="comment">#连接6666端口服务</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;client: <span class="subst">&#123;client_id&#125;</span> start, 连接到6666server&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">#开始用异步双向socket发送多个请求测试</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">5</span>):</span><br><span class="line">        request = &#123;</span><br><span class="line">            <span class="comment"># UUID = Universally Unique Identifier（通用唯一标识符），类似消息的md5</span></span><br><span class="line">            <span class="string">&quot;msg_id&quot;</span>: <span class="built_in">str</span>(uuid.uuid4()),</span><br><span class="line">            <span class="string">&quot;query&quot;</span>: <span class="string">f&quot;请求<span class="subst">&#123;i&#125;</span> 来自 Client-<span class="subst">&#123;client_id&#125;</span>&quot;</span>,</span><br><span class="line">            <span class="string">&quot;timestamp&quot;</span>: time.time(),</span><br><span class="line">        &#125;</span><br><span class="line">        socket.send_json(request)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;client-<span class="subst">&#123;client_id&#125;</span> 发送请求 <span class="subst">&#123;request[<span class="string">&#x27;msg_id&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line">        </span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 异步阻塞接收</span></span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">5</span>):</span><br><span class="line">        response = socket.recv_json()<span class="comment">#非阻塞接收</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;Client-<span class="subst">&#123;client_id&#125;</span> 收到响应：<span class="subst">&#123;response&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    socket.close()</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 class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    t_start = time.time()</span><br><span class="line"></span><br><span class="line">    engine_thread = threading.Thread(target=server_poll_with_workers,daemon=<span class="literal">True</span>)</span><br><span class="line">    engine_thread.start()</span><br><span class="line"></span><br><span class="line">    time.sleep(<span class="number">1</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;time.time() - t_start:<span class="number">.3</span>f&#125;</span>s] 服务端启动完成&quot;</span>)</span><br><span class="line"></span><br><span class="line">    client_threads = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">3</span>):</span><br><span class="line">        t = threading.Thread(target = client, args=(i,),daemon=<span class="literal">True</span>)</span><br><span class="line">        t.start()</span><br><span class="line">        client_threads.append(t)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;time.time() - t_start:<span class="number">.3</span>f&#125;</span>s] 3 个客户端已启动&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">for</span> t <span class="keyword">in</span> client_threads:</span><br><span class="line">            t.join()</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;time.time() - t_start:<span class="number">.3</span>f&#125;</span>s] 所有客户端完成&quot;</span>)</span><br><span class="line">        time.sleep(<span class="number">2</span>)</span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;over&quot;</span>)</span><br><span class="line"></span><br><span class="line">    t_end = time.time()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;======================================&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;  总耗时: <span class="subst">&#123;t_end - t_start:<span class="number">.3</span>f&#125;</span>s&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;======================================&quot;</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="PUSH-PULL-通信模式"><a href="#PUSH-PULL-通信模式" class="headerlink" title="PUSH&#x2F;PULL 通信模式"></a>PUSH&#x2F;PULL 通信模式</h2><p>这个通信模式，是无回复的，上面的REQ-REP模式是有回复的。</p><p>这个单向传输模式，适合EngineCore把decode的输出，push给LLMEngine，无需前端回复</p><p><strong>PUSH 与 PULL 是 ZeroMQ 所提供的单向数据流通信模式，构成一个无回复、单向的消息管道</strong></p><p>消息从 PUSH 端发送，由 PULL 端接收</p><p><strong>模型每推理出一个 token 就可以通过 PUSH 端发送一次，消费端 AsyncLLM 则持续监听并接收新的数据分块</strong></p><h1 id="FastAPI-uvicorn"><a href="#FastAPI-uvicorn" class="headerlink" title="FastAPI, uvicorn"></a>FastAPI, uvicorn</h1><p>这两个python库，主要学习如何用python构建web服务，来实现真正面对用户的服务器接口，是如何把服务器的接口来和我们的AsyncLLM的客户端的各种行为绑定的</p><h2 id="基础使用"><a href="#基础使用" class="headerlink" title="基础使用"></a>基础使用</h2><p>vllm里面主要掌握这几个部分的使用即可</p><ul><li><strong>基础app服务</strong><ul><li>路径参数，查询参数</li><li>指令<ul><li>GET 查询</li><li>POST 新建</li><li>PUT 完整更新</li><li>PATCH 部分更新</li></ul></li><li>http的帧协议：<ul><li><img src="/images/Pasted%20image%2020260805110133.png"></li><li>我们在curl里面，-H 可以指定修改我们的默认请求头，新增请求头，接受的时候，服务端就用查询参数的命名+ 默认Header(None)即可</li><li>-X可以指定我们的请求体，也就是结构化的自定义数据，底层会默认转成json-元组的转换</li></ul></li><li>自定义数据</li><li>请求头，Cookie<ul><li>请求头就是Header(None), 这是一个空的最基础的请求头，比如选择user_agent, 还可以新增自己的请求头，cookie&#x3D;Cookie(None), 这个Cookie也是一个Header的子类，可以帮我们拆分获取更加细致的cookie的内容。</li><li>cookie就是客户端保存的一些信息，比如上次浏览服务器的一些缓存信息, 这样一并发过来，就可以直接复用之前的信息了。</li></ul></li><li><strong>流式响应</strong>：<ul><li>这个需要对协程的理解，主要利用的是yield，把协程任务变成了可迭代的对象，之后就可以用async for 来异步迭代访问这个迭代对象，从而实现一个流式的响应（在这里，fastapi的库给我们封装了一个StreamingResponse方法，用来实现async for的异步迭代访问）</li></ul></li></ul></li><li><strong>具有lifespan的app服务</strong><ul><li>这个就是具有构造和析构逻辑的app服务，vllm在这个构造和析构里面来构造我们的大模型推理环境，这样就可以直接进行generate()推理了。</li></ul></li></ul><h2 id="demo"><a href="#demo" class="headerlink" title="demo"></a>demo</h2><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">  </span><br><span class="line"></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">基础使用：路径参数，查询参数</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> FastAPI</span><br><span class="line"></span><br><span class="line">app = FastAPI()</span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义根路径的GET路由</span></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">root</span>():</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;message&quot;</span>: <span class="string">&quot;hello world&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/items/&#123;item_id&#125;&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">read_item</span>(<span class="params">item_id: <span class="built_in">int</span>, q: <span class="built_in">str</span> | <span class="literal">None</span> = <span class="literal">None</span></span>):</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;item_id&quot;</span>: item_id, <span class="string">&quot;q&quot;</span>: q&#125;</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 class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">post新建，put完整更新指令 + 自定义数据</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="keyword">from</span> pydantic <span class="keyword">import</span> BaseModel</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义请求体的数据模型</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Item</span>(<span class="title class_ inherited__">BaseModel</span>):</span><br><span class="line">    name: <span class="built_in">str</span></span><br><span class="line">    description: <span class="built_in">str</span> | <span class="literal">None</span> = <span class="literal">None</span></span><br><span class="line">    price: <span class="built_in">float</span></span><br><span class="line">    tax: <span class="built_in">float</span> | <span class="literal">None</span> = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/items/&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">create_item</span>(<span class="params">item: Item</span>):</span><br><span class="line">    <span class="keyword">return</span> item <span class="comment"># 创建一个新的对象</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.put(<span class="params"><span class="string">&quot;/items/&#123;item_id&#125;&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">update_item</span>(<span class="params">item_id: <span class="built_in">int</span>, item: Item</span>):</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;item_id&quot;</span>: item_id, <span class="string">&quot;item_name&quot;</span>: item.name&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">请求头+ Cookie</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> Header, Cookie, Request</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/items/header/&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">test_head</span>(<span class="params">user_agent: <span class="built_in">str</span> = Header(<span class="params"><span class="literal">None</span></span>), session_token: <span class="built_in">str</span> = Cookie(<span class="params"><span class="literal">None</span></span>)</span>):</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;User-Agent&quot;</span>: user_agent, <span class="string">&quot;Session-Token&quot;</span>: session_token&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/items/header/all&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">all_headers</span>(<span class="params">request: Request</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">dict</span>(request.headers)  <span class="comment"># 返回全部请求头</span></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"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">══════════════════════════════════════════</span></span><br><span class="line"><span class="string">示例 ①：流式响应（SSE）— vLLM 逐 token 返回的核心</span></span><br><span class="line"><span class="string">══════════════════════════════════════════</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">from</span> fastapi.responses <span class="keyword">import</span> StreamingResponse</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/chat/stream&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">chat_stream</span>(<span class="params">prompt: <span class="built_in">str</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;模拟 vLLM 的流式输出：逐 token 推给客户端&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">generate</span>():</span><br><span class="line">        tokens = [<span class="string">&quot;你&quot;</span>, <span class="string">&quot;好&quot;</span>, <span class="string">&quot;，&quot;</span>, <span class="string">&quot;我是&quot;</span>, <span class="string">&quot;AI&quot;</span>, <span class="string">&quot;，&quot;</span>, prompt]</span><br><span class="line">        <span class="keyword">for</span> token <span class="keyword">in</span> tokens:</span><br><span class="line">            <span class="keyword">yield</span> <span class="string">f&quot;data: <span class="subst">&#123;token&#125;</span>\n\n&quot;</span>  <span class="comment"># SSE 格式（Server-Sent Events）</span></span><br><span class="line">            <span class="keyword">await</span> asyncio.sleep(<span class="number">0.3</span>)    <span class="comment"># 模拟推理耗时</span></span><br><span class="line">        <span class="keyword">yield</span> <span class="string">&quot;data: [DONE]\n\n&quot;</span>         <span class="comment"># 结束标记</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">    可迭代对象：</span></span><br><span class="line"><span class="string">        能for x in obj 的就是，列表，字符串，文件都是</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    yield</span></span><br><span class="line"><span class="string">        函数里面的return是一次性全给，yield是一次给一个，暂停，等你取走，继续</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">                def normal():</span></span><br><span class="line"><span class="string">                    return [1, 2, 3]     # 一次性全给，函数结束</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">                def gen():</span></span><br><span class="line"><span class="string">                    yield 1               # 给一个，暂停</span></span><br><span class="line"><span class="string">                    yield 2               # 再给一个，暂停</span></span><br><span class="line"><span class="string">                    yield 3               # 再给一个，结束</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">                # 用它</span></span><br><span class="line"><span class="string">                for num in gen():         # for 循环逐个取</span></span><br><span class="line"><span class="string">                    print(num)            # 1 → 2 → 3，每次循环取一个 yield</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        内存上利差别大：return 100万个 token 先攒好；yield 每生成一个就立刻给出去，不攒。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">        </span></span><br><span class="line"><span class="string">    async for </span></span><br><span class="line"><span class="string">            普通for 在取下一个的时候必须阻塞， async for可以停下等待去处理其他请求</span></span><br><span class="line"><span class="string">            async for, 以及async def generate，都可以看成是协程任务了，这样生成器不会阻塞，主线程迭代for也不会阻塞</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    &#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 第一个参数必须是能够被迭代的对象。</span></span><br><span class="line">    <span class="comment"># generate()是一个异步生成器，也就是async def + yield</span></span><br><span class="line">    <span class="keyword">return</span> StreamingResponse(generate(), media_type=<span class="string">&quot;text/event-stream&quot;</span>)</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 class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">══════════════════════════════════════════</span></span><br><span class="line"><span class="string">示例 ②：lifespan — vLLM 的方式：启动时创建引擎，关闭时清理</span></span><br><span class="line"><span class="string">══════════════════════════════════════════</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">from</span> contextlib <span class="keyword">import</span> asynccontextmanager</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 用 lifespan 替代直接 FastAPI()，vLLM 就是这样做的（api_server.py:173-179）</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@asynccontextmanager </span><span class="comment"># 让yield从生成器切换成切分点</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">lifespan</span>(<span class="params">app: FastAPI</span>):</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">    启动程序 = async with 的__aenter__，  就是构造函数</span></span><br><span class="line"><span class="string">    &#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;&quot;&quot;模拟 vLLM 的 lifespan：启动建引擎 → 运行 → 关闭清理&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[lifespan] 启动中... 初始化 AsyncLLM（模拟）&quot;</span>)</span><br><span class="line">    <span class="keyword">await</span> asyncio.sleep(<span class="number">1</span>)</span><br><span class="line">    app.state.engine = &#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;ready&quot;</span>&#125;    <span class="comment"># 挂到 app.state，handler 能取</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[lifespan] 引擎就绪，开始对外服务&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 切分点</span></span><br><span class="line">    <span class="keyword">yield</span>                                      <span class="comment"># ← 这里暂停，服务运行中</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">    结束程序 = async with 的__aexit__， 就是析构函数</span></span><br><span class="line"><span class="string">    &#x27;&#x27;&#x27;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[lifespan] 关闭中... 清理引擎&quot;</span>)</span><br><span class="line">    <span class="keyword">await</span> asyncio.sleep(<span class="number">0.5</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[lifespan] 已关闭&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 注意：用了 lifespan 后之前的 app 要改成这样，这里单独建一个演示用</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 这是一个带构造和析构逻辑的一个新的app， lifespan就是指定构造析构函数</span></span><br><span class="line">app_with_lifespan = FastAPI(lifespan=lifespan)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@app_with_lifespan.get(<span class="params"><span class="string">&quot;/engine/status&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">engine_status</span>(<span class="params">request: Request</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;handler 通过 app.state 取引擎状态&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> request.app.state.engine</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试: uvicorn api_server:app_with_lifespan --port 8001</span></span><br><span class="line"><span class="comment">#       curl http://localhost:8001/engine/status</span></span><br><span class="line"><span class="comment">#       按 Ctrl+C 看 lifespan 的 shutdown 日志</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="python协程"><a href="#python协程" class="headerlink" title="python协程"></a>python协程</h1><p>cpu调度的最小单位是线程，所以协程仅仅存在于线程里面，依赖于asyncio这个事件循环库。</p><p>可以简单理解为依托于这个线程，实现了一个多任务系统，类似rtos，引入协程任务的概念</p><blockquote><p>协程任务和我们的主线程是分离开的，互不影响，就用rtos的概念来理解即可</p></blockquote><h2 id="定义一个协程任务task"><a href="#定义一个协程任务task" class="headerlink" title="定义一个协程任务task"></a>定义一个协程任务task</h2><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">task_A</span>():</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">task_B</span>():</span><br><span class="line"><span class="comment"># xxx</span></span><br><span class="line"><span class="keyword">await</span> task_A()</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">asyncio.run(task_B())</span><br><span class="line"></span><br><span class="line"><span class="comment">#fastapi里面支持的绑定服务接口</span></span><br><span class="line"><span class="meta">@app.get(<span class="params"><span class="string">&quot;/items/&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">task_C</span>():</span><br><span class="line">reutrn</span><br></pre></td></tr></table></figure><p>以上就是一个最简单的协程任务的基础用法</p><ul><li>async 来定义一个协程任务</li><li>await来阻塞等待一个协程任务，并挂起自己这个协程任务</li><li>asyncio.run来启动事件循环（启动所有任务）</li><li>fastapi里面支持把服务器接口和协程任务绑定，这样可以异步调用。</li></ul><h1 id="python的一些语法"><a href="#python的一些语法" class="headerlink" title="python的一些语法"></a>python的一些语法</h1><h2 id="weakref-finalize-方法"><a href="#weakref-finalize-方法" class="headerlink" title="weakref.finalize(方法)"></a>weakref.finalize(方法)</h2><ul><li>weakref， python的弱引用库<ul><li>可以在不增加对象引用的前提下，访问对象</li></ul></li><li>weakref.finalize, 注册一个对象的清理回调工具<ul><li>可以用来注册对象被销毁退出时候的钩子函数</li></ul></li></ul><h2 id="python多进程编程"><a href="#python多进程编程" class="headerlink" title="python多进程编程"></a>python多进程编程</h2><ul><li>spawn<ul><li>创建一个新的 Python 解释器， 绕开GIL。  </li><li>速度比 fork 或者 forkserver 模式慢。  </li><li>windows | macOS 下的默认模式， unix 也支持。  </li><li>并非继承（或者拷贝）父进程的全部资源，而是主动传入进程对象run方法所需的资源，子进程会拷贝一份传递进来的资源</li><li><mark style="background:#fff88f">总结来说，就是深拷贝，直接把传入的对象指针的内存进行拷贝</mark><ul><li><mark style="background:#fff88f">因此不可以传递：文件对象|文件句柄、线程锁，如果需要，则需要另外创建</mark></li></ul></li></ul></li><li>fork<ul><li>就是单纯的浅拷贝 + Cow， 子进程修改的时候才会重新申请内存进行深拷贝</li></ul></li><li>forkserver</li></ul><p>fork和spawn都是构建子进程的不同方式，区别在于：</p><p><mark style="background:#fff88f">fork：</mark><strong>除了必要的启动资源外，其他变量，包，数据等都继承自父进程，并且是copy-on-write的，也就是共享了父进程的一些内存页，因此启动较快，但是由于大部分都用的父进程数据，所以是不安全的进程</strong></p><p><mark style="background:#fff88f">spawn</mark>：<strong>从头构建一个子进程，父进程的数据等拷贝到子进程空间内，拥有自己的Python解释器，所以需要重新加载一遍父进程的包，因此启动较慢，由于数据都是自己的，安全性较高</strong></p><p>实际使用中可以根据子进程具体做什么来选取用fork还是spawn~</p><h2 id="Python-GIL"><a href="#Python-GIL" class="headerlink" title="Python GIL"></a>Python GIL</h2><h3 id="GIL工作原理"><a href="#GIL工作原理" class="headerlink" title="GIL工作原理"></a>GIL工作原理</h3><p>在Python中，<a href="https://zhida.zhihu.com/search?content_id=241506919&content_type=Article&match_order=1&q=%E5%85%A8%E5%B1%80%E8%A7%A3%E9%87%8A%E5%99%A8%E9%94%81&zhida_source=entity">全局解释器锁</a>（<code>Global Interpreter Lock，简称GIL</code>）</p><p>GIL是Python解释器中的一个互斥锁，<mark style="background:#fff88f">它确保在同一时刻只有一个线程能够执行Python字节码</mark></p><p>这意味着在多线程环境下，Python解释器无法同时利用多个CPU核心进行并行执行，因为只有一个线程能够执行Python<a href="https://zhida.zhihu.com/search?content_id=241506919&content_type=Article&match_order=1&q=%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4&zhida_source=entity">字节码指令</a></p><p>当Python解释器运行Python代码时，它会获取GIL，然后执行相应的字节码指令。其他线程想要执行Python字节码时，必须先获取GIL，但只有在当前线程释放GIL后才能获得。因此，只有一个线程能够在任意时刻执行Python字节码，这就是GIL的工作原理</p><h3 id="多线程的影响"><a href="#多线程的影响" class="headerlink" title="多线程的影响"></a>多线程的影响</h3><p>GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 （比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行）</p><p>有一点要强调的是GIL只会影响到那些严重依赖CPU的程序（比如计算型的）。 如果你的程序大部分只会涉及到I&#x2F;O，比如网络交互，那么使用多线程就很合适， 因为它们大部分时间都在等待</p><h3 id="多进程的影响"><a href="#多进程的影响" class="headerlink" title="多进程的影响"></a>多进程的影响</h3><p>GIL只是影响多线程的并发执行同一段代码</p><p>在 Python 中，GIL（全局解释器锁）只影响到了多线程，而不会对多进程产生直接的影响。多进程是通过创建不同的 Python 解释器来实现的，因此每个进程都有自己的独立 GIL，它们之间互不影响</p><h2 id="vllm的系统属性的实现"><a href="#vllm的系统属性的实现" class="headerlink" title="vllm的系统属性的实现"></a>vllm的系统属性的实现</h2><ul><li><p><code>os.environ</code> 是操作系统环境变量的一个 <strong>dict-like 对象</strong>， <strong>用来保存我们的属性项和具体的内容</strong></p><ul><li>正常我们在添加到这里面的对象后，通过</li><li>os.environ.get(“VLLM_WORKER_MULTIPROC_METHOD”)</li><li>os.getenv(“VLLM_WORKER_MULTIPROC_METHOD”)</li><li>来读取这个属性</li></ul></li><li><p>vLLM 提供了一种<strong>懒读取</strong>的封装：写 <code>envs.VLLM_WORKER_MULTIPROC_METHOD</code>，不用手动写 <code>os.getenv(&quot;VLLM_WORKER_MULTIPROC_METHOD&quot;)</code></p></li></ul><h2 id="OMP-NUM-THREADS的实现"><a href="#OMP-NUM-THREADS的实现" class="headerlink" title="OMP_NUM_THREADS的实现"></a>OMP_NUM_THREADS的实现</h2><p><code>OMP_NUM_THREADS</code> 是 <strong>OpenMP 的标准环境变量</strong>，控制并行区域中的线程数。PyTorch 底层在 CPU 上做矩阵运算时会用到它：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 跟 vLLM 无关，任何使用 OpenMP 的程序都认这个变量</span></span><br><span class="line">$ OMP_NUM_THREADS=<span class="number">4</span> python my_script.py</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">set_multiprocessing_worker_envs</span>(<span class="params">local_world_size: <span class="built_in">int</span> = <span class="number">1</span></span>):</span><br><span class="line">    _maybe_force_spawn()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 两种情况直接跳过，不设置：</span></span><br><span class="line">    <span class="comment"># ① CPU 平台：不需要 GPU worker 的线程管理</span></span><br><span class="line">    <span class="comment"># ② 用户已经手动设好了 $OMP_NUM_THREADS</span></span><br><span class="line">    <span class="keyword">if</span> current_platform.is_cpu() <span class="keyword">or</span> <span class="string">&quot;OMP_NUM_THREADS&quot;</span> <span class="keyword">in</span> os.environ:</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 否则，vLLM 自动计算一个合理值并写进 os.environ</span></span><br><span class="line">    num_threads = startup_omp_num_threads(local_world_size)</span><br><span class="line">    os.environ[<span class="string">&quot;OMP_NUM_THREADS&quot;</span>] = <span class="built_in">str</span>(num_threads)   <span class="comment"># ← 主动注入</span></span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Python 标准库能做的：</span></span><br><span class="line"><span class="keyword">import</span> threading</span><br><span class="line">t = threading.Thread(target=work)    <span class="comment"># ← Python 层的线程</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Python 标准库管不了的：</span></span><br><span class="line"><span class="comment"># PyTorch 底层是 C++ 写的，内部有自己的 OpenMP 线程池</span></span><br><span class="line">a @ b   <span class="comment"># ← 矩阵乘法，背后 C++ 开了 N 个 OpenMP 线程并行计算</span></span><br></pre></td></tr></table></figure><p><code>torch.set_num_threads()</code> 控制的是<strong>后者</strong>——PyTorch C++ 底层在 CPU 上做运算时会开多少个线程。Python 的 <code>threading</code> 模块对此一无所知。</p><h2 id="网络通信"><a href="#网络通信" class="headerlink" title="网络通信"></a>网络通信</h2><p>网络通信4要素</p><figure class="highlight armasm"><table><tr><td class="code"><pre><span class="line"><span class="symbol">tcp:</span><span class="comment">//127.0.0.1:29500</span></span><br><span class="line"> ─┬─   ───┬───  ──┬──</span><br><span class="line">  │       │       └── 端口号 (Port)：找到机器上的哪个服务</span><br><span class="line">  │       └── <span class="built_in">IP</span> 地址 (Address)：找到网络上的哪台机器</span><br><span class="line">  └── 协议 (Protocol)：用什么方式传数据</span><br></pre></td></tr></table></figure><table><thead><tr><th>要素</th><th>是什么</th><th>你刚看过的代码</th></tr></thead><tbody><tr><td><strong>协议</strong></td><td>TCP &#x2F; UDP &#x2F; IPC &#x2F; inproc</td><td><code>tcp://</code></td></tr><tr><td><strong>IP 地址</strong></td><td>哪台机器</td><td><code>get_loopback_ip()</code> → <code>127.0.0.1</code></td></tr><tr><td><strong>端口号</strong></td><td>机器上的哪个程序</td><td><code>get_open_port()</code> → <code>29500</code></td></tr><tr><td><strong>Socket</strong></td><td>操作系统提供的读写接口，把上面三个绑在一起</td><td><code>socket.socket()</code> → <code>bind()</code> → <code>listen()</code> &#x2F; <code>connect()</code></td></tr></tbody></table><p> <strong>跟 vLLM 代码的对应关系</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># ① 拼出地址字符串（URL）</span></span><br><span class="line">url = get_distributed_init_method(get_loopback_ip(), get_open_port())</span><br><span class="line"><span class="comment">#  → &quot;tcp://127.0.0.1:29500&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ② 把地址字符串传给 PyTorch，PyTorch 内部帮你做 socket bind/connect</span></span><br><span class="line">torch.distributed.init_process_group(backend=<span class="string">&quot;nccl&quot;</span>, init_method=url)</span><br><span class="line"><span class="comment">#  ↑ 这个函数内部：</span></span><br><span class="line"><span class="comment">#     - Master 进程：socket() → bind(&quot;127.0.0.1&quot;, 29500) → listen() → accept()</span></span><br><span class="line"><span class="comment">#     - Worker 进程：socket() → connect(&quot;127.0.0.1&quot;, 29500)</span></span><br><span class="line"><span class="comment">#     - 握手完成后 → 释放 rendezvous socket</span></span><br></pre></td></tr></table></figure><p>URL 是<strong>地址字符串</strong>，告诉对方”去哪找我”。Socket 是<strong>操作系统提供的读写工具</strong>，真正用来收发字节。URL 是地图上的坐标，Socket 是敲门的动作。</p><h3 id="网络协议层级"><a href="#网络协议层级" class="headerlink" title="网络协议层级"></a>网络协议层级</h3><figure class="highlight dns"><table><tr><td class="code"><pre><span class="line">应用层    HTTP, gRPC, Redis Protocol, PyTorch Rendezvous ...</span><br><span class="line">         ─────────────────────────────────────────────────</span><br><span class="line">传输层    TCP, UDP, IPC, inproc</span><br><span class="line">         ─────────────────────────────────────────────────</span><br><span class="line">网络层    IP (<span class="number">127.0.0.1</span>, <span class="number">192.168</span>.x.x)</span><br></pre></td></tr></table></figure><p><strong>传输层</strong>管”数据怎么传过去”——可靠还是不可靠、面向连接还是无连接。<br><strong>应用层</strong>管”传过去的数据什么意思”——是网页请求还是数据库查询。</p><p>在 <a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/utils/network_utils.py">network_utils.py</a> 里你能找到这些：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># ① TCP — 可靠、面向连接、可以跨机器</span></span><br><span class="line"><span class="string">&quot;tcp://127.0.0.1:29500&quot;</span>        <span class="comment"># get_distributed_init_method()</span></span><br><span class="line">socket.SOCK_STREAM               <span class="comment"># TCP socket</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ② UDP — 不可靠、无连接、只管发包不管对方收没收到</span></span><br><span class="line">socket.SOCK_DGRAM                <span class="comment"># test_loopback_bind() 里用的</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ③ IPC — Unix Domain Socket，只能本机通信，不走网络栈，比 TCP 快</span></span><br><span class="line"><span class="string">&quot;ipc:///tmp/vllm/xxxx&quot;</span>          <span class="comment"># get_open_zmq_ipc_path()</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ④ inproc — 同进程内通信，零拷贝，最快</span></span><br><span class="line"><span class="string">&quot;inproc://xxxx&quot;</span>                 <span class="comment"># get_open_zmq_inproc_path()</span></span><br></pre></td></tr></table></figure><p>正常ipc, inproc是不属于网络协议的，但是python 的 ZMQ把他兼容进来了</p><p>就是 <strong>ZMQ（ZeroMQ）</strong>——一个把各种通信方式统一成同一种 API 的库。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 不管底层是什么，用法完全一样</span></span><br><span class="line">zmq_socket.bind(<span class="string">&quot;tcp://127.0.0.1:29500&quot;</span>)</span><br><span class="line">zmq_socket.bind(<span class="string">&quot;ipc:///tmp/vllm/xxxx&quot;</span>)</span><br><span class="line">zmq_socket.bind(<span class="string">&quot;inproc://xxxx&quot;</span>)</span><br><span class="line"><span class="comment">#               ↑</span></span><br><span class="line"><span class="comment">#          协议前缀不同，代码一样</span></span><br></pre></td></tr></table></figure><p>ZMQ 用一个统一的 <code>zmq://</code> 格式把它们包在同一套 API 里。你用 <code>bind(&quot;inproc://foo&quot;)</code> 还是 <code>bind(&quot;tcp://bar&quot;)</code>，代码一模一样，ZMQ 在背后帮你处理底层差异。</p><p>可以这样简单理解</p><ul><li>context &#x3D; zmq.Context()<ul><li>构造zmq的上下文，其实就是socket发送接受的缓冲区</li></ul></li><li>socket &#x3D; context.socket(zmq.PUB)<ul><li>构造一个套接字socket, 这个套接字，你就理解成是持有这种通信机制的输入输出的缓冲区，代替你来实现网络通信的实例对象，发送接受调用它的方法来。</li></ul></li></ul><h2 id="共享内存，zmq-PUB-SUB-多进程环境继承，编码utf-8"><a href="#共享内存，zmq-PUB-SUB-多进程环境继承，编码utf-8" class="headerlink" title="共享内存，zmq.PUB-SUB, 多进程环境继承，编码utf-8"></a>共享内存，zmq.PUB-SUB, 多进程环境继承，编码utf-8</h2><p>我做了一个实验，可以从代码里面很简单学明白这些概念</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> zmq</span><br><span class="line"><span class="keyword">import</span> multiprocessing</span><br><span class="line"><span class="keyword">import</span> multiprocessing.shared_memory <span class="keyword">as</span> sm</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">import</span> struct</span><br><span class="line"></span><br><span class="line">SM_SIZE = <span class="number">4096</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">executor</span>(<span class="params">read_pipe, write_pipe</span>):</span><br><span class="line">    <span class="comment"># 创建共享内存</span></span><br><span class="line">    sm_buf = sm.SharedMemory(create=<span class="literal">True</span>, size=SM_SIZE)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[executor] create shared-memory name = <span class="subst">&#123;sm_buf.name&#125;</span>, size = <span class="subst">&#123;SM_SIZE&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建ZMQ的PUB套接字，用于网络的发布通信</span></span><br><span class="line">    context = zmq.Context()<span class="comment"># zmq缓冲区</span></span><br><span class="line">    pub = context.socket(zmq.PUB)</span><br><span class="line">    pub.bind(<span class="string">&quot;tcp://127.0.0.1:15555&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[executor] ZMQ PUB bind localhost:15555&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 连接信息，发送给子进程</span></span><br><span class="line">    handle = &#123;</span><br><span class="line">        <span class="string">&quot;sm_name&quot;</span>: sm_buf.name,</span><br><span class="line">        <span class="string">&quot;zmq_pub_addr&quot;</span>: <span class="string">&quot;tcp://127.0.0.1:15555&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 构造子进程，启动</span></span><br><span class="line">    proc = multiprocessing.Process(target=worker, args=(write_pipe, handle))</span><br><span class="line">    proc.start()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 等待子进程启动，注册好共享内存，SUB connect PUB </span></span><br><span class="line">    read_pipe.recv() <span class="comment"># 等待子进程写入 ready</span></span><br><span class="line"></span><br><span class="line">    time.sleep(<span class="number">0.2</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[executor] 收到子进程ready消息&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">3</span>):</span><br><span class="line">        msg = <span class="string">f&quot;req #<span class="subst">&#123;i&#125;</span>&quot;</span>.encode(<span class="string">&quot;utf-8&quot;</span>) <span class="comment"># 转16进制字符串</span></span><br><span class="line">        data_len = <span class="built_in">len</span>(msg)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 写入共享内存</span></span><br><span class="line">        sm_buf.buf[<span class="number">0</span>:<span class="number">4</span>] = struct.pack(<span class="string">&quot;&gt;I&quot;</span>, data_len) <span class="comment"># 定长</span></span><br><span class="line">        sm_buf.buf[<span class="number">4</span>:<span class="number">4</span>+data_len] = msg</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[executor] 写入共享内存: <span class="subst">&#123;msg.decode()&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 表示发送了两个帧（独立发送的字节块），所以用[]列表来表示</span></span><br><span class="line">        <span class="comment"># PUB/SUB 里第一帧自动被当成话题，用于路由匹配，后续帧是自由数据。</span></span><br><span class="line">        <span class="comment"># b&quot;task&quot; = &quot;task&quot;.encode(&quot;utf-8&quot;), 且只对纯ascll字符有效</span></span><br><span class="line">        <span class="comment"># &quot;新任务来了&quot;.encode(), encode为空，默认转成utf-8</span></span><br><span class="line">        <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">        unicode 是个 编号表，给世界上每个字符分配一个唯一数字，不涉及怎么存</span></span><br><span class="line"><span class="string">        &#x27;A&#x27; → U+0041  (65)</span></span><br><span class="line"><span class="string">        &#x27;你&#x27; → U+4F60  (20320)， 所以都是一个数字</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        至于这个数字，被对应到什么16进制字节，那就是编码。</span></span><br><span class="line"><span class="string">        有的编码 2 字节，有的 4 字节，有的变长。所以同一个字在不同平台可能有不同存储方式</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        pyzmq 其实能接受字符串，内部会帮你用 UTF-8 编码。你把 .encode() 去掉也能跑。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        那为什么要自己写？两个原因：一是有几十种编码，万一某天你要对接一个 GBK 或 GB2312 的系统，不显式指定会全部乱码。</span></span><br><span class="line"><span class="string">        二是代码清晰——看到 .encode() 就知道这里发生了编码转换，不用去猜 pyzmq 偷偷做了什么</span></span><br><span class="line"><span class="string">        &#x27;&#x27;&#x27;</span></span><br><span class="line">        pub.send_multipart([<span class="string">b&quot;task&quot;</span>, <span class="string">&quot;新任务来了&quot;</span>.encode()]) <span class="comment"># 发送两个帧，第一个默认被当成话题</span></span><br><span class="line"></span><br><span class="line">        time.sleep(<span class="number">0.5</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    sm_buf.buf[<span class="number">0</span>:<span class="number">4</span>] = struct.pack(<span class="string">&quot;&gt;I&quot;</span>, <span class="number">0</span>)</span><br><span class="line">    pub.send_multipart([<span class="string">b&quot;stop&quot;</span>,<span class="string">&quot;结束了&quot;</span>.encode()]) <span class="comment"># 发送结束了的话题</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[executor] 发送停止信号&quot;</span>)</span><br><span class="line"></span><br><span class="line">    proc.join(timeout=<span class="number">3</span>)</span><br><span class="line">    <span class="keyword">if</span> proc.is_alive():</span><br><span class="line">        proc.terminate()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 释放资源</span></span><br><span class="line">    sm_buf.close()</span><br><span class="line">    sm_buf.unlink()</span><br><span class="line">    pub.close()</span><br><span class="line">    context.term()</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 class="keyword">def</span> <span class="title function_">worker</span>(<span class="params">write_pipe, handle</span>):</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 注册好共享内存</span></span><br><span class="line">    sm_buf = sm.SharedMemory(name=handle[<span class="string">&quot;sm_name&quot;</span>])</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[worker] 注册好 共享内存 name = <span class="subst">&#123;sm_buf.name&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># SUB 连接 PUB</span></span><br><span class="line">    context = zmq.Context()</span><br><span class="line">    sub = context.socket(zmq.SUB)</span><br><span class="line">    sub.connect(handle[<span class="string">&quot;zmq_pub_addr&quot;</span>])</span><br><span class="line">    sub.setsockopt_string(zmq.SUBSCRIBE, <span class="string">&quot;task&quot;</span>) <span class="comment"># 订阅task话题</span></span><br><span class="line">    sub.setsockopt_string(zmq.SUBSCRIBE, <span class="string">&quot;stop&quot;</span>) <span class="comment"># 订阅stop话题</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[worker] ZMQ SUB 连接: <span class="subst">&#123;handle[<span class="string">&#x27;zmq_pub_addr&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    write_pipe.send(<span class="string">&quot;ready&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        topic, body = sub.recv_multipart() <span class="comment"># 阻塞等待多帧数据，第一帧作为话题，剩下的是消息内容</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[worker] ZMQ 收到 topic = <span class="subst">&#123;topic.decode()&#125;</span>, body=<span class="subst">&#123;body.decode()&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> topic == <span class="string">b&quot;stop&quot;</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;[worker] 收到stop 停止&quot;</span>)</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">        <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">        [0]，因为struct.unpack返回的是元组</span></span><br><span class="line"><span class="string">            struct.unpack(&quot;&gt;I&quot;, b&#x27;\x00\x00\x00\x05&#x27;)    # → (5,)     ← 元组！</span></span><br><span class="line"><span class="string">            struct.unpack(&quot;&gt;II&quot;, b&#x27;\x00\x00\x00\x01\x00\x00\x00\x02&#x27;)  # → (1, 2)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        buf[0:4]是memoryview切片，</span></span><br><span class="line"><span class="string">        写.tobytes()，是把共享内存里面的数据复制到本地bytes，确保unpack不会被并发干扰</span></span><br><span class="line"><span class="string">        &#x27;&#x27;&#x27;</span></span><br><span class="line">        data_len = struct.unpack(<span class="string">&quot;&gt;I&quot;</span>, sm_buf.buf[<span class="number">0</span>:<span class="number">4</span>].tobytes())[<span class="number">0</span>]</span><br><span class="line">        msg = sm_buf.buf[<span class="number">4</span>:<span class="number">4</span>+data_len].tobytes().decode(<span class="string">&quot;utf-8&quot;</span>) <span class="comment"># 从utf-8解码成unicode</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[worker] 从共享内存读到任务: <span class="subst">&#123;msg&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    sm_buf.close()</span><br><span class="line">    sub.close()</span><br><span class="line">    context.term()</span><br><span class="line">    write_pipe.close()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># 用spawn创建多进程，不继承资源</span></span><br><span class="line">    multiprocessing.set_start_method(<span class="string">&quot;spawn&quot;</span>, <span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建一个单向的内核缓冲区（管道）</span></span><br><span class="line">    read_pipe, write_pipe = multiprocessing.Pipe(duplex=<span class="literal">False</span>)</span><br><span class="line">    executor(read_pipe, write_pipe)</span><br><span class="line">    <span class="comment">#read_pipe.close() # 这个由子进程释放</span></span><br><span class="line">    write_pipe.close()</span><br></pre></td></tr></table></figure><p>最终输出为：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">(vllm_study) liangji@liangjideMacBook-Air test5 % python my_zmq_sh.py</span><br><span class="line">[executor] create shared-memory name = psm_a6f60e1b, size = 4096</span><br><span class="line">[executor] ZMQ PUB <span class="built_in">bind</span> localhost:15555</span><br><span class="line">[worker] 注册好 共享内存 name = psm_a6f60e1b</span><br><span class="line">[worker] ZMQ SUB 连接: tcp://127.0.0.1:15555</span><br><span class="line">[executor] 收到子进程ready消息</span><br><span class="line">[executor] 写入共享内存: req <span class="comment">#0</span></span><br><span class="line">[worker] ZMQ 收到 topic = task, body=新任务来了</span><br><span class="line">[worker] 从共享内存读到任务: req <span class="comment">#0</span></span><br><span class="line">[executor] 写入共享内存: req <span class="comment">#1</span></span><br><span class="line">[worker] ZMQ 收到 topic = task, body=新任务来了</span><br><span class="line">[worker] 从共享内存读到任务: req <span class="comment">#1</span></span><br><span class="line">[executor] 写入共享内存: req <span class="comment">#2</span></span><br><span class="line">[worker] ZMQ 收到 topic = task, body=新任务来了</span><br><span class="line">[worker] 从共享内存读到任务: req <span class="comment">#2</span></span><br><span class="line">[executor] 发送停止信号</span><br><span class="line">[worker] ZMQ 收到 topic = stop, body=结束了</span><br><span class="line">[worker] 收到stop 停止</span><br></pre></td></tr></table></figure><h2 id="future包装器"><a href="#future包装器" class="headerlink" title="future包装器"></a>future包装器</h2><p><img src="/images/Pasted%20image%2020260812210715.png"></p><p>vLLM 自己写的，继承的 <code>Future</code> 来自 Python 标准库：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> concurrent.futures <span class="keyword">import</span> Future  <span class="comment"># Python 标准库</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">FutureWrapper</span>(<span class="title class_ inherited__">Future</span>):           <span class="comment"># vLLM 自己扩展</span></span><br></pre></td></tr></table></figure><p>标准库 <code>Future</code> 只管”设结果 + 等结果”。vLLM 加的那层是自定义的 FIFO 有序约束——<code>result()</code> 里 drain 队列清掉更老的 future 这一整段。就是自己实现的，不是用的现成库。</p><blockquote><p>注意，这里的future机制的理解。这边再自己说一下：</p><p>future机制，是异步RPC，也就是异步远程过程调用的机制，他的作用在于把RPC的发送调用（右侧），得到调用结果（左侧）解耦</p><p>这样executor 发出RPC 调用给worker,<br> 无需等待，直到executor调用future.result()才开始阻塞获取这个RPC的结果。<br> vllm 的 futurewrapper， 在future的基础上 ， <code>result()</code> 的阻塞 + FIFO drain 来保证顺序。<br> <img src="/images/Pasted%20image%2020260812211546.png"></p><p> <mark style="background:#fff88f">就是如果我按照 1,2,3的RPC顺序挂出去，然后读取3的结果，就必须要获得1，2的结果，保证了RPC的输出一定是按照RPC发出的顺序</mark></p></blockquote><h2 id="python-随机种子"><a href="#python-随机种子" class="headerlink" title="python 随机种子"></a>python 随机种子</h2><p>计算机里的「随机数」不是真随机，而是<strong>伪随机</strong>——从一个初始数字出发，用确定性的算法吐出一串看起来随机的序列。这个初始数字就叫 <strong>seed（种子）</strong>。</p><p>关键性质：<strong>同一个 seed → 吐出来的随机序列完全一样</strong>。所以 seed 是「可复现」的开关。</p><p><code>set_random_seed</code> 是「给所有随机数生成器统一设定起点」的工具函数</p><p>因为 vLLM 的代码里会用到好几套随机数来源（Python 的 <code>random</code>、numpy、torch、以及 GPU 上的 CUDA 随机），所以要把它们<strong>全部</strong>设成同一个 seed，才能保证「整条推理链是确定性的」</p><h2 id="python-的GC"><a href="#python-的GC" class="headerlink" title="python 的GC"></a>python 的GC</h2><p><code>gc</code> 是 Python 标准库的 <strong>garbage collector（垃圾回收）模块</strong>，<code>gc.collect()</code> 就是「手动强制立刻跑一次垃圾回收」。</p><p><strong>Python 的内存管理有两套机制</strong></p><p>Python 回收内存不是靠一个统一机制，而是两层：</p><table><thead><tr><th>机制</th><th>管什么</th><th>什么时候触发</th></tr></thead><tbody><tr><td><strong>引用计数</strong></td><td>普通对象，引用数归 0 立即释放</td><td>自动、即时</td></tr><tr><td><strong>循环 GC（<code>gc</code> 模块）</strong></td><td><strong>循环引用</strong>的对象（A 指向 B、B 又指向 A）</td><td>周期触发，或手动 <code>gc.collect()</code></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr></tbody></table><p>关键点：<strong>循环引用</strong>这种对象，因为互相引用、引用计数永远不为 0，靠引用计数根本释放不掉。所以 Python 另外搞了一个「三色标记」的循环检测器，就是 <code>gc</code> 模块干的事。</p><p><code>gc.collect()</code> &#x3D; <strong>别等它周期触发了，现在就立刻扫一遍，把所有循环引用的垃圾清掉</strong>。</p><h2 id="NUMA"><a href="#NUMA" class="headerlink" title="NUMA"></a>NUMA</h2><p>NUMA(Non-Uniform Memory Access)，即非一致性内存访问，是一种关于多个CPU如何访问内存的架构模型</p><p><img src="/images/Pasted%20image%2020260814102436.png" alt="572"></p><p><img src="/images/Pasted%20image%2020260814102506.png"></p><p><img src="/images/Pasted%20image%2020260814102601.png"></p><h3 id="参数变量"><a href="#参数变量" class="headerlink" title="参数变量"></a>参数变量</h3><p>先建立一个核心概念：</p><ul><li><code>args</code>：普通位置参数<ul><li>def add(a, b): 函数，然后调用add(1,2), 这里a&#x3D;1, b&#x3D;2就是位置参数</li></ul></li><li><code>kwargs</code>：关键字参数<ul><li>调用 add(a &#x3D; 1, b &#x3D; 2), 这里的a&#x3D;1， b&#x3D;2 就是关键字参数</li></ul></li><li><code>*args</code>：把多个位置参数打包成 tuple<ul><li>def func(<code>*args</code>)</li><li>func(1,2,3,4), 这个时候<code>*args</code> 就是 1，2，3，4 这4个数</li><li><code>*</code>表示解包，那么args就是打包后的变量，所以args &#x3D; (1,2,3,4), 所以args[0] &#x3D; 1</li><li>这个的好处是，不需要知道参数的个数，也不用指定名字</li><li><code>*args</code> 收集的是<strong>没有通过关键字形式传递的位置参数（positional arguments）</strong></li></ul></li><li><code>**kwargs</code>：把多个关键字参数打包成 dict<ul><li>def func (<code>**kwargs</code>)</li><li>func(name&#x3D;”qwen”, size&#x3D;”7B”)</li><li>所以kwargs&#x3D;{“name”:”qwen”, “size”: “7B”} 就是字典，所以，kwargs[“dtype”] &#x3D; fp16, 可以直接拿关键字</li></ul></li><li><code>*</code> 和 <code>**</code> 在调用时表示解包</li></ul><h2 id="hugging-face模型中各文件"><a href="#hugging-face模型中各文件" class="headerlink" title="hugging face模型中各文件"></a>hugging face模型中各文件</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">(base) liangji@ubun:~/huggingface/Qwen2.5-7B-Instruct-1M$ tree</span><br><span class="line">.</span><br><span class="line">├── config.json</span><br><span class="line">├── generation_config.json</span><br><span class="line">├── LICENSE</span><br><span class="line">├── merges.txt</span><br><span class="line">├── model-00001-of-00004.safetensors</span><br><span class="line">├── model-00002-of-00004.safetensors</span><br><span class="line">├── model-00003-of-00004.safetensors</span><br><span class="line">├── model-00004-of-00004.safetensors</span><br><span class="line">├── model.safetensors.index.json</span><br><span class="line">├── model_structure.txt</span><br><span class="line">├── README.md</span><br><span class="line">├── sparse_attention_config.json</span><br><span class="line">├── tokenizer_config.json</span><br><span class="line">├── tokenizer.json</span><br><span class="line">└── vocab.json</span><br></pre></td></tr></table></figure><h3 id="模型权重"><a href="#模型权重" class="headerlink" title="模型权重"></a>模型权重</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">├── model-00001-of-00004.safetensors</span><br><span class="line">├── model-00002-of-00004.safetensors</span><br><span class="line">├── model-00003-of-00004.safetensors</span><br><span class="line">├── model-00004-of-00004.safetensors</span><br></pre></td></tr></table></figure><p>模型权重的分片文件，safetensors是一种安全，快速的张量存储格式，是目前HuggingFace推荐的格式，比传统的pytorch的.bin格式更加安全。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">├── model.safetensors.index.json</span><br></pre></td></tr></table></figure><p>这个是索引文件&#x2F;地图，当模型权重被分片时，这个文件会告诉HuggingFace的transformers库，每个具体的权重存储在哪一个.safetensors分片文件中。</p><h3 id="模型配置"><a href="#模型配置" class="headerlink" title="模型配置"></a>模型配置</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">├── config.json</span><br></pre></td></tr></table></figure><p>这个定义了模型的架构，包含了模型的各种超参数。</p><ul><li>模型类型：architectures， eg: LlamaForCausalLM</li><li>隐藏层大小: hidden_size</li><li>注意力头的数量：num_attention_heads</li><li>网络层数：num_hidden_layers</li><li>词表大小：vocab_size</li></ul><p>当我们使用<code>AutoModel.from_pretrained(...)</code>, 库首先会读取这个config.json, 了解是什么模型结构，然后再去加载权重 。</p><p><img src="/images/Pasted%20image%2020260816153505.png" alt="465"></p><p><img src="/images/Pasted%20image%2020260816153520.png" alt="603"></p><p><img src="/images/Pasted%20image%2020260816153533.png" alt="476"></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">configuration.json</span><br></pre></td></tr></table></figure><p>这个是config.json的旧版，一样的</p><h3 id="分词器文件"><a href="#分词器文件" class="headerlink" title="分词器文件"></a>分词器文件</h3><p>分词器是负责 把 文字 - token id的 双向转换</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">tokenizer.json</span><br></pre></td></tr></table></figure><p>这个是由 HuggingFace 的 tokenizers 库生成的一体化文件，包含了分词器所需的全部信息</p><ul><li>词表</li><li>合并规则</li><li>特殊tokens</li></ul><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">tokenizer_config.json</span><br></pre></td></tr></table></figure><p>这个文件配置了分词器类的行为，定义了如何使用其他文件，以及一些特殊token的名称，比如</p><ul><li>bos_token （句子开头）</li><li>eos_token( (句子结尾)</li><li>unk_token (未知词)</li></ul><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">vocab.json</span><br></pre></td></tr></table></figure><p>词汇表文件，就是个json字典，将每个token 映射到唯一的 token id</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">merges.txt</span><br></pre></td></tr></table></figure><p>这个是BPE分词算法的合并规则文件，按照优先级顺序列出了如何将子词合并成更大的token<br>e r-&gt; er</p><p><strong>文件关系：</strong></p><p><code>AutoTokenizer.from_pretrained(...) </code>会智能地加载这些文件。如果存在 <code>tokenizer.json</code>，它会优先使用这个文件，<mark style="background:#fff88f">因为它最快最全</mark>。如果不存在，它会根据 tokenizer_config.json 的指示，组合 vocab.json 和 merges.txt 等文件来构建分词器</p><h3 id="生成与聊天配置文件"><a href="#生成与聊天配置文件" class="headerlink" title="生成与聊天配置文件"></a>生成与聊天配置文件</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">generation_config.json</span><br></pre></td></tr></table></figure><p>这个文件为模型的文本生成过程提供了默认参数。当你调用 .generate() 方法时，如果没有指定参数，就会使用这里的配置。常见的参数包括：</p><ul><li>max_length：<mark style="background:#fff88f">生成文本的最大长度</mark>。</li><li>temperature, top_p, top_k：<mark style="background:#fff88f">控制生成文本多样性和创造性的参数</mark>。</li><li>do_sample：<mark style="background:#fff88f">是否使用采样策略</mark>。</li></ul><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">chat_template.json</span><br></pre></td></tr></table></figure><p>对于聊天模型（Chat Model）来说，这是一个非常重要的文件。它定义了如何将多轮对话（包含系统提示、用户输入、模型回复）格式化为模型能够理解的单个字符串。这通常是一个Jinja2模板，确保了角色和特殊token（如 [INST], </s>）被正确地放置。如果格式不对，聊天模型的效果会大打折扣</p><h3 id="其他文件"><a href="#其他文件" class="headerlink" title="其他文件"></a>其他文件</h3><figure class="highlight dos"><table><tr><td class="code"><pre><span class="line">README.<span class="built_in">md</span></span><br></pre></td></tr></table></figure><p>作用：这是一个Markdown格式的说明文件，也就是“模型卡片”（Model Card）。它详细介绍了模型的信息，包括：模型描述、用途、限制、如何使用、训练数据、评测结果等。这是用户了解和使用模型的首要入口。</p><figure class="highlight asciidoc"><table><tr><td class="code"><pre><span class="line"><span class="title">.gitattributes</span></span><br></pre></td></tr></table></figure><p>作用：这是一个Git的配置文件。它通常与 git-lfs (Large File Storage) 一起使用，告诉Git如何处理大文件。对于像 .safetensors 这样的GB级大文件，这个文件会指示Git只跟踪文件的指针，而不是文件的完整内容，从而让仓库保持轻量。</p><figure class="highlight pgsql"><table><tr><td class="code"><pre><span class="line">preprocessor_config.json</span><br></pre></td></tr></table></figure><p>作用：预处理器配置文件。对于纯文本的语言模型，这个文件可能比较简单或不存在。但在多模态模型（如处理图像或音频）中，它会定义数据预处理的步骤，例如图像缩放尺寸、归一化参数等。</p><h2 id="HuggingFace的库"><a href="#HuggingFace的库" class="headerlink" title="HuggingFace的库"></a>HuggingFace的库</h2><p>Hugging Face 本身就是一个开源 AI 生态组织&#x2F;平台，最核心的是提供了一整套模型与推理训练库</p><p><img src="/images/Pasted%20image%2020260816153734.png" alt="579"></p><p><img src="/images/Pasted%20image%2020260816153805.png" alt="433"></p><p><img src="/images/Pasted%20image%2020260816153821.png"></p><p><img src="/images/Pasted%20image%2020260816154110.png" alt="334"><br><img src="/images/Pasted%20image%2020260816154129.png"></p><p><img src="/images/Pasted%20image%2020260816154248.png" alt="599"></p><h2 id="pytorch的nn-Module-nn-Parameters"><a href="#pytorch的nn-Module-nn-Parameters" class="headerlink" title="pytorch的nn.Module, nn.Parameters"></a>pytorch的nn.Module, nn.Parameters</h2><ul><li>nn.Module<ul><li>是神经网络模块的基类</li><li>它本身不是张量，而是一个「容器&#x2F;管理器」：负责组织参数、子模块、buffer，管理 <code>forward</code>、device、dtype、训练&#x2F;推理状态、hook 等。你自定义的层都要继承它，才能被 <code>named_children()</code>、<code>state_dict()</code>、<code>.to(device)</code> 这些机制识别</li></ul></li><li>nn.Parameter<ul><li>这个是torch.Tensor的子类</li><li>它唯一做的事，就是给张量打个「我是参数」的标记。被标记后，<code>nn.Module</code> 会特殊对待它</li></ul></li></ul><h2 id="torch-Tensor-numel"><a href="#torch-Tensor-numel" class="headerlink" title="torch.Tensor.numel()"></a>torch.Tensor.numel()</h2><p>返回张量里所有元素的个数</p><h1 id="vllm-（AsyncLLM前端-EngineCore后端）"><a href="#vllm-（AsyncLLM前端-EngineCore后端）" class="headerlink" title="vllm （AsyncLLM前端 + EngineCore后端）"></a>vllm （AsyncLLM前端 + EngineCore后端）</h1><h2 id="0-架构设计"><a href="#0-架构设计" class="headerlink" title="0_架构设计"></a>0_架构设计</h2><p>简单来说，我们的这个vllm，分成3个部分</p><ul><li><mark style="background:#fff88f">api server</mark><ul><li><mark style="background:#fff88f">引擎前端（AsyncLLM）</mark></li><li><mark style="background:#fff88f">引擎后端（EngineCore）</mark></li></ul></li></ul><p><strong>api server</strong> ，就是uvicorn + fastapi, 负责作为服务，来响应用户的需求。<br><strong>引擎前端</strong>，用来接受用户的响应，分词，然后发送给对应的引擎后端<br><strong>引擎后端</strong>，用来真正干活，推理</p><p>三者各司其职，从而构建真正的<mark style="background:#fff88f">在线推理服务</mark>。</p><ul><li><strong>在线</strong>：引擎前端<ul><li>实时处理不同的输入，不是固定的离线输入</li></ul></li><li><strong>推理</strong>：引擎后端<ul><li>推理</li></ul></li><li><strong>服务</strong>：api server<ul><li>包装在线前后端引擎，让用户真正能使用，作为一个http服务器</li></ul></li></ul><h2 id="1-vllm-的-api-server的启动"><a href="#1-vllm-的-api-server的启动" class="headerlink" title="1_vllm 的 api server的启动"></a>1_vllm 的 api server的启动</h2><p>从vllm这个整体项目的启动，到api server的服务启动，到引擎前后端的启动过程梳理。</p><p>这一节将从整个vllm的项目运行vllm -&gt; vllm serve -&gt; 启动引擎 -&gt; fastapi+uvicorn启动服务器来分析api server的启动。</p><h3 id="api-server架构"><a href="#api-server架构" class="headerlink" title="api server架构"></a>api server架构</h3><p>下面先看一下整体的一个启动步骤</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">① main.py:17  main()</span><br><span class="line">    │</span><br><span class="line">    ├─ 23:   import serve模块</span><br><span class="line">    ├─ 27-34: CMD_MODULES 列表（含 serve 模块）</span><br><span class="line">    ├─ 70-80: 创建 argparser + subparsers</span><br><span class="line">    │</span><br><span class="line">    ├─ 82-86: for每个模块 → 调 cmd_init() → 遍历返回的Cmd对象</span><br><span class="line">    │            cmd.subparser_init(subparsers)    注册argparse子命令</span><br><span class="line">    │            set_defaults(dispatch_function=cmd.cmd)   ← 关键: 绑定执行函数</span><br><span class="line">    │</span><br><span class="line">    ├─ 87:   parser.parse_args()  解析命令行</span><br><span class="line">    │        用户敲 &quot;vllm serve&quot; → args.subparser=&quot;serve&quot;, dispatch_function=ServeSubcommand.cmd</span><br><span class="line">    │</span><br><span class="line">    └─ 92:   args.dispatch_function(args)  ← 直接调绑定的函数</span><br><span class="line">                  │</span><br><span class="line">                  ▼</span><br><span class="line">② serve.py:43  ServeSubcommand.cmd(args)    ← 静态方法，真正的业务入口</span><br><span class="line">    │</span><br><span class="line">    ├─ 54-62:  处理 --headless 分支</span><br><span class="line">    ├─ 64-104: 处理 DP / LB / elastic EP 参数（优化相关，跳过）</span><br><span class="line">    │</span><br><span class="line">    ├─ 115-116: api_server_count &lt; 1 → run_headless(args)</span><br><span class="line">    ├─ 117-118: api_server_count &gt; 1 → run_multi_api_server(args)</span><br><span class="line">    │</span><br><span class="line">    └─ 119-122: 默认（单 API server）→ uvloop.run(run_server(args))</span><br><span class="line">                      │</span><br><span class="line">                      ▼</span><br><span class="line">③ openai/api_server.py:686  async def run_server(args)</span><br><span class="line">    └─ 696: run_server_worker()</span><br><span class="line">         ├─ 707: build_async_engine_client() → AsyncLLM.from_vllm_config()  ← 前端+引擎</span><br><span class="line">         └─ 711: build_and_serve() → serve_http() → uvicorn 监听           ← HTTP</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h3><p>下面来逐步分析api server的启动源码</p><h4 id="pyproject-toml"><a href="#pyproject-toml" class="headerlink" title="pyproject.toml"></a>pyproject.toml</h4><p>首先是vllm&#x2F;下面的我们的整个vllm项目的一个项目构建文件，类似CMakeLists.txt<br><code>pyproject.toml</code></p><p><img src="/images/Pasted%20image%2020260807100105.png"></p><p>可以看到，我们这里指定我们的项目叫vllm, 项目入口是<code>vllm/entrypoints/cli/main.py</code>的main()函数。</p><hr><h4 id="vllm源码目录结构"><a href="#vllm源码目录结构" class="headerlink" title="vllm源码目录结构"></a>vllm源码目录结构</h4><p>这里先大致预览一下<strong>vllm里面每个目录的相关内容</strong>是什么</p><table><thead><tr><th>路径</th><th>作用</th></tr></thead><tbody><tr><td><strong>entrypoints&#x2F;</strong></td><td>各种”入口”：CLI、OpenAI API、离线推理、MCP服务</td></tr><tr><td><strong>v1&#x2F;</strong></td><td><strong>V1 引擎（主力引擎）</strong>：调度器、执行器、采样、KV cache</td></tr><tr><td><strong>engine&#x2F;</strong></td><td>旧的 V0 引擎接口（兼容层，正在被 V1 替代）</td></tr><tr><td><strong>model_executor&#x2F;</strong></td><td>模型实现：各架构的前向计算（GPU 侧，超大）</td></tr><tr><td><strong>config&#x2F;</strong></td><td>配置体系：VllmConfig &#x2F; EngineArgs 等 dataclass 定义</td></tr><tr><td><strong>distributed&#x2F;</strong></td><td>分布式通信：TP&#x2F;PP&#x2F;DP 的 NCCL、进程组管理</td></tr><tr><td><strong>compilation&#x2F;</strong></td><td>torch.compile &#x2F; dynamo &#x2F; inductor 相关代码</td></tr><tr><td><strong>outputs.py</strong></td><td>RequestOutput 等返回给用户的数据类型</td></tr><tr><td><strong>plugins&#x2F;</strong></td><td>插件机制：entry point 加载</td></tr><tr><td><strong>inputs&#x2F;</strong></td><td>EngineInput 等输入数据类型</td></tr><tr><td><strong>multimodal&#x2F;</strong></td><td>多模态处理：图片&#x2F;视频&#x2F;音频预处理</td></tr><tr><td><strong>lora&#x2F;</strong></td><td>LoRA 适配器管理</td></tr><tr><td><strong>tokenizers&#x2F;</strong></td><td>tokenizer 加载与封装</td></tr><tr><td><strong>utils&#x2F;</strong></td><td>通用工具函数</td></tr><tr><td><strong>platforms&#x2F;</strong></td><td>硬件平台抽象（CUDA&#x2F;ROCm&#x2F;CPU&#x2F;TPU）</td></tr><tr><td><strong>kernels&#x2F;</strong></td><td>手写 CUDA kernel 和 PyTorch 自定义算子</td></tr><tr><td><strong>usage&#x2F;</strong></td><td>使用量统计（遥测上报）</td></tr><tr><td><strong>logger.py</strong></td><td>日志初始化</td></tr><tr><td><strong>version.py</strong></td><td>版本号</td></tr><tr><td><strong>envs.py</strong></td><td>所有环境变量常量定义</td></tr><tr><td><strong>sampling_params.py</strong></td><td>SamplingParams &#x2F; PoolingParams 类定义（用户传的采样配置）</td></tr></tbody></table><hr><h4 id="main-py"><a href="#main-py" class="headerlink" title="main.py"></a>main.py</h4><p>下面我们看到main()里面，就是我们执行vllm指令运行的地方，先是注册了一些子命令模块<br><img src="/images/Pasted%20image%2020260807100845.png" alt="541"></p><p>把工厂函数获取到的各个子命令对象实例，注册到cmds里面，</p><p><img src="/images/Pasted%20image%2020260807100949.png"></p><p><img src="/images/Pasted%20image%2020260807101040.png" alt="327"></p><p>所以我们执行vllm serve，实际上是PC指针运行到<code>vllm/entrypoints/cli/serve.py</code>里面</p><p><img src="/images/Pasted%20image%2020260807101209.png" alt="608"></p><p>这里的dispatch_function就是在执行具体的各个子命令类对象的绑定的执行函数，也就是子命令类里面的一个静态方法：<br><img src="/images/Pasted%20image%2020260807101345.png" alt="519"></p><hr><h4 id="serve-py"><a href="#serve-py" class="headerlink" title="serve.py"></a>serve.py</h4><p>下面来分析serve.py具体做了什么，当我们执行了vllm serve后，PC指针跳转到cmd()来执行</p><p>有效代码如下：可以看到，主要干了以下几件事情：</p><ul><li>获取模型地址</li><li>确认api server的启动数量</li><li>启动api server<ul><li>-&gt; 我们默认单api server, <code>run_server()</code><br><img src="/images/Pasted%20image%2020260807101925.png"><br><img src="/images/Pasted%20image%2020260807101948.png"></li></ul></li></ul><p>这里的uvloop.run 就是 高性能版本的 asyncio.run, 开启一个协程任务，之后，我们的这个进程的PC指针到这里就结束了</p><p><img src="/images/Pasted%20image%2020260807102410.png"></p><p><img src="/images/Pasted%20image%2020260807102447.png"></p><p>而我们的run_server则是在<code>vllm/entrypoints/openai/api_server.py</code>里面定义，因为我们要启动的是openai接口的api server</p><hr><h4 id="api-server-py"><a href="#api-server-py" class="headerlink" title="api_server.py"></a>api_server.py</h4><p><img src="/images/Pasted%20image%2020260807102609.png"></p><p>这就是我们的第一个协程任务，开始启动api server</p><p>我们先申请好api server的地址，端口号，socket，防止被占用。</p><p><img src="/images/Pasted%20image%2020260807102843.png"></p><ul><li>valid_api_server_args 检查args命令行参数的合法性</li><li>占用host地址 + 端口号</li><li>创建好socket套接字</li><li>定义好退出信号和回调</li><li>返回 服务器地址，端口号，socket套接字</li></ul><p>之后就是<code>run_server_worker</code>， 来阻塞等待启动服务器</p><hr><p>启动服务器 api server (uvicorn)<br><img src="/images/Pasted%20image%2020260807103232.png"></p><p>可以看到，这个第二个串行的协程任务里面，又异步协程任务：</p><ul><li>build_async_engine_client<ul><li>阻塞在build_and_serve</li></ul></li></ul><p>之后我们的第二个协程任务就阻塞在等待shutdown_task的信号（等待服务结束）</p><p>因此PC指针主要开始在</p><ul><li>build_async_engine_client</li><li>build_and_serve</li></ul><hr><h5 id="构造引擎-build-async-engine-client"><a href="#构造引擎-build-async-engine-client" class="headerlink" title="构造引擎 build_async_engine_client"></a>构造引擎 build_async_engine_client</h5><p>下面来看一下build_async_engine_client， 是如何来构造我们的引擎前端的。传入参数是命令行的参数 + 客户端的配置， 返回的是我们构造出来的引擎前端engine_client</p><p><img src="/images/Pasted%20image%2020260807103829.png"></p><ul><li>AsyncEngineArgs.from_cli_args获取我们的引擎参数</li><li>如果配置了引擎前端的配置<ul><li>引擎前端进程 总数</li><li>当前index</li></ul></li></ul><blockquote><p>这里要说明的是，引擎前端的数量，和我们的api server的服务进程是绑定的，也就是一个api server的服务进程，产生一个引擎前端。</p><p>这样的好处是，我们就可以实现<br><img src="/images/Pasted%20image%2020260805230426.png"><br>一个端口好，服务器有4个服务进程来处理输入，并行化程度增高，所以这里的client_config， 只是在告诉本进程的这个协程任务，你这个进程是第几个服务进程，以及总共多少个协程进程</p></blockquote><p>之后，就是启动一个新的协程这来真正创建引擎前端，并抛出</p><p>至于这里为什么是yield engine, 而不是return engine, 原因在于我们创建出来的engine-client的实例，需要一个自动回收释放的逻辑。</p><p>看一下build_async_engine_client_from_engine_args的逻辑</p><p><img src="/images/Pasted%20image%2020260807104835.png"></p><ul><li>vllm_config 是根据配置创造出来的配置实例</li><li>开始调用我们的vllm&#x2F;v1&#x2F;engine&#x2F;async_llm.py的AsyncLLM的类方法</li></ul><p>注意这里用try-finally来构造好一个AsyncLLM的引擎前端实例，然后用yield抛出。这样的好处是暂停这个build_async_engine_client_from_engine_args的后续逻辑，因此，针对这个async_llm的实例对象，协程的finally的自动回收机制，还是可以保留，后续被async with的回收engine_client触发的finally的回收机制</p><p><img src="/images/Pasted%20image%2020260807105335.png"></p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">build_and_serve:</span><br><span class="line">  async with build_async_engine_client(args) as engine_client:    ← 入口</span><br><span class="line">    │</span><br><span class="line">    ├─► build_async_engine_client:</span><br><span class="line">    │     async with build_async_engine_client_from_engine_args(...) as engine:</span><br><span class="line">    │       │</span><br><span class="line">    │       ├─► build_async_engine_client_from_engine_args:</span><br><span class="line">    │       │     async_llm = AsyncLLM.from_vllm_config(...)</span><br><span class="line">    │       │     yield async_llm                    ← 暂停点②</span><br><span class="line">    │       │</span><br><span class="line">    │       ├─ 拿到 engine</span><br><span class="line">    │       │   yield engine                         ← 暂停点①</span><br><span class="line">    │       │</span><br><span class="line">    ├─ 拿到 engine_client</span><br><span class="line">    │   ... build_app ... serve_http ...              ← 跑很久</span><br><span class="line">    │   ... 收到 SIGTERM ... serve_http 返回 ...</span><br><span class="line">    │</span><br><span class="line">    │ ← async with 块结束 ─────────────────────────── 退出！</span><br><span class="line">    │       │</span><br><span class="line">    │       ├─ 回到暂停点① 下面（build_async_engine_client）</span><br><span class="line">    │       │   async with 块退出</span><br><span class="line">    │       │     │</span><br><span class="line">    │       │     └─► 回到暂停点② 下面（build_async_engine_client_from_engine_args）</span><br><span class="line">    │       │           finally: async_llm.shutdown()  ← 回收！</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>关键：<code>serve_http</code> 一返回，<code>build_and_serve</code> 里最外层的 <code>async with</code> 块就退出了。退出时 Python 自动逐层”解开”暂停的协程——先唤醒 <code>build_async_engine_client</code>（它的 <code>yield</code> 下面没有 finally，直接过），它的 <code>async with</code> 退出触发内层 <code>build_async_engine_client_from_engine_args</code> 的 <code>finally</code>，执行 <code>shutdown()</code>。</p><p>就是你之前学的 <code>ctrl+c → shutdown_task → serve_http 结束 → async with 逐层退出 → finally 回收 → done</code>。</p><h6 id="python的上下文管理器"><a href="#python的上下文管理器" class="headerlink" title="python的上下文管理器"></a>python的上下文管理器</h6><p>所以这也算是一个标准的<strong>python上下文管理器</strong><br><img src="/images/Pasted%20image%2020260807110035.png"></p><p><img src="/images/Pasted%20image%2020260807110218.png"></p><h5 id="构造-启动服务器-build-and-serve"><a href="#构造-启动服务器-build-and-serve" class="headerlink" title="构造&#x2F;启动服务器 build_and_serve"></a>构造&#x2F;启动服务器 build_and_serve</h5><p>既然上面利用上下文管理器，来创建了我们这个api server进程的engine_client, 也就是async_llm的对象实例，下面就可以开始根据这个对象实例，开始构建我们的这个api  server进程，开始真正干活了</p><p>输入:引擎前端实例，主机地址，socket套接字<br>输出:一个asyncio的task</p><p><img src="/images/Pasted%20image%2020260807145319.png"></p><ul><li>阻塞查询引擎前端实例支持的任务类型，是纯文本还是多模态</li><li>获取模型的配置参数</li><li>创建app<ul><li>这里只是创建了lifespan的构造析构逻辑，没有指定真正的引擎前端</li></ul></li><li>阻塞init_app_state， 开始把app和engine_client绑定在一起，注册到app.state里面。</li><li>serve_http， 开始启动api server</li></ul><h5 id="build-app-lifespan"><a href="#build-app-lifespan" class="headerlink" title="build_app &amp; lifespan"></a>build_app &amp; lifespan</h5><p>这里来介绍一下，他是如何构建fastapi 的 app的</p><p><img src="/images/Pasted%20image%2020260807145641.png"></p><p><img src="/images/Pasted%20image%2020260807145742.png" alt="510"><br>这里可以看到：</p><ul><li>app &#x3D; FastAPI(……, lifespan &#x3D; lifespan)</li></ul><p>指定的lifespan的逻辑被写到了 vllm&#x2F;entrypoints&#x2F;openai&#x2F;server_utils的lifespan方法里面</p><p><img src="/images/Pasted%20image%2020260807145836.png"></p><p>可以看到，正常来说，我们api server启动app，增加lifespan是用来增加构造析构服务的。但是注意，我们增加的构造准备，析构回收逻辑，是针对服务本身的，<strong>所以这里的lifespan是增加了一个后台的定时清理任务</strong>。而我们的真正服务器的核心逻辑，在引擎engine_client(async_llm)被构造的时候，init()的时候，就已经构造启动好了。</p><p>简单看一下这里的lifespan的逻辑</p><blockquote><p>@asynccontextmanager 异步上下文管理器<br>这是一个装饰器，修饰下面的方法，可以把yield生成器变成上下文的隔断符，这样就相当于上面是服务的构造逻辑，下半段是服务的析构逻辑<br><img src="/images/Pasted%20image%2020260807150331.png"><br>因此当我们启动app的时候，就会自动创建一个后台任务，结束这个app服务的时候，就可以销毁这个后台任务。</p></blockquote><blockquote><p>另外这里还有一个 freeze_gc_heap()的方法。<br>这个的作用是，冻结GC的堆，也就是GC只扫描服务开启后，请求处理的创建的新的对象，避免启动期间的对象被重复扫描回收。</p></blockquote><hr><p>既然我们已经准备好了app服务的启动和回收。</p><p>那么接下来肯定要丰富app服务的各种接口功能，这样才能实现一个服务器</p><p><img src="/images/Pasted%20image%2020260807150736.png"></p><p>我们主要是这一条，router其实就是路由，也就是fastapi的那个接口的地址，比如&#x2F;items&#x2F;什么的。这样就知道发过来的请求是哪个接口来处理。<br><img src="/images/Pasted%20image%2020260807150800.png"></p><p>这里就是给我们的app 服务，注册一个generate的任务类型，具体定义在<br><code>vllm/entrypoints/openai/generate/api_router.py -&gt; register_generate_api_routers()</code></p><p>下面看一下具体的注册内容<br><img src="/images/Pasted%20image%2020260807151055.png"></p><p>“generate” 是<strong>任务类型</strong>的名称，不是接口路径。意思就是”这个模型能生成文本”。</p><p>对应到具体接口，全部走 OpenAI 标准命名：</p><table><thead><tr><th>接口路径</th><th>用途</th></tr></thead><tbody><tr><td><code>POST /v1/chat/completions</code></td><td><strong>主力</strong> — ChatGPT 风格对话</td></tr><tr><td><code>POST /v1/completions</code></td><td>老式文本补全（续写）</td></tr><tr><td><code>POST /v1/responses</code></td><td>OpenAI 新协议（Responses API）</td></tr><tr><td><code>POST /v1/messages</code></td><td>Anthropic Claude 兼容格式</td></tr></tbody></table><p>再加之前注册的：</p><table><thead><tr><th>接口路径</th><th>用途</th></tr></thead><tbody><tr><td><code>GET /v1/models</code></td><td>列出模型</td></tr><tr><td><code>GET /health</code></td><td>健康检查</td></tr><tr><td><code>POST /v1/tokenize</code></td><td>分词</td></tr><tr><td><code>POST /v1/embeddings</code></td><td>向量嵌入（embedding 模型才有）</td></tr></tbody></table><p>核心就是 <code>/v1/chat/completions</code>，绝大部分用户用这一个就够了。</p><hr><p>现在来分析这个 &#x2F;v1&#x2F;chat&#x2F;completions这个router</p><p><img src="/images/Pasted%20image%2020260807151643.png"></p><p>这个 router 里就<strong>两个</strong>接口：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@router.post(<span class="params"><span class="string">&quot;/v1/chat/completions&quot;</span></span>)           </span><span class="comment"># 单条对话</span></span><br><span class="line"><span class="meta">@router.post(<span class="params"><span class="string">&quot;/v1/chat/completions/batch&quot;</span></span>)     </span><span class="comment"># 批量对话</span></span><br></pre></td></tr></table></figure><p><code>app.include_router(router)</code> 会把 router 里定义的全部路由一次性挂到 app 上。和你学习时的写法等价于：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/v1/chat/completions&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">create_chat_completion</span>(<span class="params">...</span>): ...</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/v1/chat/completions/batch&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">create_batch_chat_completion</span>(<span class="params">...</span>): ...</span><br></pre></td></tr></table></figure><p>只是用 <code>APIRouter</code> 把相关接口封装在一个独立的模块里，每个模块只注册少量路由。不是单文件写几十个 <code>@app.get/post</code>。</p><p><img src="/images/Pasted%20image%2020260807151906.png"></p><p><img src="/images/Pasted%20image%2020260807152142.png"></p><p>下面罗列一下build_app里面注册的所有模块和各自的接口</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">build_app</span><br><span class="line">│</span><br><span class="line">├── ① 基础设施（无条件注册）</span><br><span class="line">│   ├── register_vllm_serve_api_routers    → vLLM 专属</span><br><span class="line">│   │   ├── lora/          LoRA 管理</span><br><span class="line">│   │   ├── tokenize/      分词</span><br><span class="line">│   │   ├── profile/       性能分析</span><br><span class="line">│   │   ├── cache/         KV 缓存管理</span><br><span class="line">│   │   ├── sleep/         休眠/唤醒</span><br><span class="line">│   │   └── rpc/           内部 RPC</span><br><span class="line">│   ├── register_models_api_router         → GET /v1/models</span><br><span class="line">│   └── register_sagemaker_api_router      → SageMaker 兼容接口</span><br><span class="line">│</span><br><span class="line">├── ② generate 能力 → 有 generate 才注册</span><br><span class="line">│   ├── register_generate_api_routers      → 核心文本生成</span><br><span class="line">│   │   ├── chat/           POST /v1/chat/completions</span><br><span class="line">│   │   ├── completion/     POST /v1/completions</span><br><span class="line">│   │   ├── responses/      POST /v1/responses</span><br><span class="line">│   │   └── anthropic/      POST /v1/messages</span><br><span class="line">│   ├── attach_disagg_router               → 分离式推理</span><br><span class="line">│   ├── attach_rlhf_router                 → RLHF</span><br><span class="line">│   ├── elastic_ep_attach_router           → MoE 弹性调度</span><br><span class="line">│   └── register_generative_scoring_router → 评分模型</span><br><span class="line">│</span><br><span class="line">├── ③ 条件注册（按 supported_tasks）</span><br><span class="line">│   ├── render/          generate 或 render 任务</span><br><span class="line">│   ├── speech_to_text/    transcription 任务</span><br><span class="line">│   ├── realtime/          realtime 任务</span><br><span class="line">│   └── pooling/           embedding/classify/等 pooling 任务</span><br><span class="line">│</span><br><span class="line">├── ④ 中间件 + 异常处理</span><br><span class="line">│   ├── CORS 中间件</span><br><span class="line">│   ├── API Key 认证</span><br><span class="line">│   ├── 异常处理器 (HTTP/Validation/Engine/Generation/Generic)</span><br><span class="line">│   └── Scaling 中间件</span><br><span class="line"></span><br></pre></td></tr></table></figure><h6 id="app接口逻辑：-v1-chat-completions"><a href="#app接口逻辑：-v1-chat-completions" class="headerlink" title="app接口逻辑： &#x2F;v1&#x2F;chat&#x2F;completions"></a>app接口逻辑： &#x2F;v1&#x2F;chat&#x2F;completions</h6><p>这个chat completions， 就是<strong>聊天完成</strong>功能，有：</p><ul><li>非流式</li><li>流式<br>两种输出模式</li></ul><p>下面看一下这个&#x2F;v1&#x2F;chat&#x2F;completions的接口的内容<br><img src="/images/Pasted%20image%2020260807152543.png"><br>在路由里面，定义好了，这是一个POST提交的指令，触发的任务是create_chat_completion</p><hr><p>handler 从chat()方法中获取一个app.state的属性，里面存储了一个OpenAIServingChat实例对象</p><blockquote><p>这个属性是在build_app后面的init_app_state里面放进去的，但是chat的接口逻辑（使用到这个属性实例）先被注册进去</p></blockquote><p><code>chat()</code> 就是一个<strong>从 app.state 取东西的快捷函数</strong>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 路由里定义的上方</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">chat</span>(<span class="params">request: Request</span>) -&gt; OpenAIServingChat | <span class="literal">None</span>:</span><br><span class="line">    <span class="keyword">return</span> request.app.state.openai_serving_chat   <span class="comment"># 就是取个属性</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用</span></span><br><span class="line">handler = chat(raw_request)</span><br><span class="line"><span class="comment"># handler = app.state.openai_serving_chat</span></span><br><span class="line"><span class="comment">#        = OpenAIServingChat 实例（在 init_generate_state 时创建好的）</span></span><br></pre></td></tr></table></figure><p><code>handler</code> 就是 <code>OpenAIServingChat</code> 实例——之前 <code>init_generate_state</code> 里用 <code>engine_client</code> + 各种配置构造出来的服务对象。它持有 <code>engine_client</code> 引用，对外提供 <code>create_chat_completion()</code> 方法。</p><p>如果模型不支持 generate 任务，<code>state.openai_serving_chat</code> 就是 <code>None</code> → <code>handler is None</code> → 接口返回 <code>501 Not Implemented</code>。</p><hr><p>这个接口<strong>同时支持流式和非流式</strong>，靠请求体里的 <code>stream</code> 参数区分</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">generator = <span class="keyword">await</span> handler.create_chat_completion(request, raw_request)</span><br><span class="line"><span class="comment"># generator 可能是三种东西：</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">isinstance</span>(generator, ErrorResponse):</span><br><span class="line">    <span class="keyword">return</span> JSONResponse(...)                              <span class="comment"># ❌ 出错</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">elif</span> <span class="built_in">isinstance</span>(generator, ChatCompletionResponse):</span><br><span class="line">    <span class="keyword">return</span> JSONResponse(content=generator.model_dump())   <span class="comment"># 📦 非流式：一次性返回完整 JSON</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> StreamingResponse(content=generator, ...)          <span class="comment"># 🌊 流式：返回 SSE 逐 token</span></span><br></pre></td></tr></table></figure><p>客户端控制：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 非流式（默认）→ 等全部生成完，一次性拿到完整回复</span></span><br><span class="line">curl ... -d <span class="string">&#x27;&#123;&quot;messages&quot;:[...], &quot;stream&quot;: false&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 流式 → 逐 token 推送，和你的 SSE 学习代码一样</span></span><br><span class="line">curl ... -d <span class="string">&#x27;&#123;&quot;messages&quot;:[...], &quot;stream&quot;: true&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>所以 <code>chat/completions</code> 不是”流式输出模式”，而是”聊天补全协议”——<code>stream</code> 参数只是里面一个开关，决定返回方式。底层调用同一个 <code>engine_client</code> 推理。</p><p>所以下面就需要去具体分析handler， 也就是<code>request.app.state.openai_serving_chat</code><br>这个<strong>OpenAIServingChat实例</strong>，是这个实例对象，来具体操作调用我们的engine_client引擎前端提交请求的。</p><hr><p>注册完这些接口后，返回这个app</p><hr><h6 id="中间层：接口handler，-init-app-state"><a href="#中间层：接口handler，-init-app-state" class="headerlink" title="中间层：接口handler， init_app_state"></a><strong>中间层</strong>：接口handler， init_app_state</h6><p><mark style="background:#fff88f">这个handler，来自OpenAIServingChat这个实例，并提供create_chat_completion的方法，来生成异步迭代器，用来流式输出。</mark></p><p>下面先来分析一下init_app_state, 看看是如何把这个OpenAIServingChat和app绑定的（<strong>这个就是把engine_client引擎前端和app绑定。这样接口就可以通过app的属性找到这个实例来操作引擎</strong>）</p><p><img src="/images/Pasted%20image%2020260807155248.png"></p><ul><li><p>通过engine_client获取vllm_config</p></li><li><p>各种校验</p></li><li><p>app.state属性写入：</p><ul><li><img src="/images/Pasted%20image%2020260807155403.png"></li><li><img src="/images/Pasted%20image%2020260807155532.png" alt="457"></li></ul></li><li><p>注册generate任务的相关属性实例</p><ul><li><img src="/images/Pasted%20image%2020260807155607.png"></li><li><img src="/images/Pasted%20image%2020260807155637.png"><ul><li><img src="/images/Pasted%20image%2020260807155854.png"></li><li><img src="/images/Pasted%20image%2020260807160014.png"></li></ul></li></ul></li></ul><p>可以看到，在api server的这个接口 和 engine_client的引擎前端之间，我们肯定是针对这个引擎有不用的用法的，具体的用法，就是<mark style="background:#fff88f">一个中间的类对象</mark>。</p><p>这个类对象就是<strong>OpenAIServingChat</strong>， 来实现这个chat的功能。由<strong>他来实现接口的具体逻辑，驱动引擎前端</strong>。</p><p>就是中间层，做三件事：</p><figure class="highlight stata"><table><tr><td class="code"><pre><span class="line">HTTP 请求（用户消息、参数）</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">┌─────────────────────────┐</span><br><span class="line">│   OpenAIServingChat      │  ← 中间层</span><br><span class="line">│                          │</span><br><span class="line">│  1. 协议解析              │  messages → token_ids + sampling_params</span><br><span class="line">│  2. 调用引擎              │  engine_client.<span class="keyword">generate</span>(...)</span><br><span class="line">│  3. 协议转换              │  RequestOutput → OpenAI JSON/SSE</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">       engine_client.<span class="keyword">generate</span>()    ← 引擎前端</span><br></pre></td></tr></table></figure><p>路由层只管 HTTP 的事（读请求、发响应），不知道 token 怎么打、采样参数怎么设。引擎层只管推理，不知道 OpenAI 协议长什么样。<code>OpenAIServingChat</code> 夹在中间把两边翻译对了。这就是你之前在 <code>init_generate_state</code> 看到构造它时为什么要传那么多参数——它需要所有信息来完成这两端转换。</p><p><mark style="background:#fff88f">所以这里本质上就是驱动层，中间件层（runtime层）的那种经典操作系统的设计架构</mark></p><p><img src="/images/Pasted%20image%2020260807160320.png"></p><hr><p>下面先来看一下这个中间适配器类的init参数<br><strong><code>__init__</code> 参数分三类：</strong></p><table><thead><tr><th>类别</th><th>参数</th><th>作用</th></tr></thead><tbody><tr><td>核心依赖</td><td><code>engine_client</code></td><td>引擎驱动（必须）</td></tr><tr><td></td><td><code>models</code></td><td>模型注册信息</td></tr><tr><td></td><td><code>openai_serving_render</code></td><td>模板渲染器（messages → prompt）</td></tr><tr><td>对话配置</td><td><code>chat_template</code></td><td>Jinja 对话模板</td></tr><tr><td></td><td><code>response_role</code></td><td>回复角色名（”assistant”）</td></tr><tr><td>功能开关</td><td><code>tool_parser</code></td><td>工具调用解析</td></tr><tr><td></td><td><code>reasoning_parser</code></td><td>推理过程解析</td></tr><tr><td></td><td><code>enable_auto_tools</code></td><td>自动工具调用</td></tr><tr><td>调试&#x2F;日志</td><td><code>request_logger</code></td><td>请求日志</td></tr><tr><td></td><td><code>enable_log_outputs</code></td><td>输出日志</td></tr></tbody></table><hr><p>下面是我们的产生SSE流式输出的异步迭代器的方法create_chat_completion</p><p><img src="/images/Pasted%20image%2020260807161326.png"></p><p><strong><code>_with_kv_transfer_rejection_cleanup</code></strong> 是优化相关：当使用远程 KV 缓存预取时，如果请求在到达引擎前就被拒绝（比如校验不通过），需要通知远端”这块缓存不用了，可以释放”。就是一个兜底清理—请求失败了别让远端一直锁着那块显存。你自己实现可以完全不用关心。</p><p><mark style="background:#fff88f">关于传入的request， 和 raw_request这两个参数：</mark><br><img src="/images/Pasted%20image%2020260807161538.png"></p><h6 id="对话模版格式转换"><a href="#对话模版格式转换" class="headerlink" title="对话模版格式转换"></a>对话模版格式转换</h6><p>下面看一下这边的格式换行<br><code>/v1/chat/completions</code> 和 <code>/v1/completions</code> 的请求体不一样：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="comment">// /v1/chat/completions — 带 role 的多轮对话</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;messages&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;system&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你是一个助手&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;user&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你好&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;assistant&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你好！有什么可以帮你？&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;user&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;今天天气怎么样&quot;</span><span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="comment">// /v1/completions — 纯文本续写，没有 role</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;prompt&quot;</span><span class="punctuation">:</span> <span class="string">&quot;今天天气真不错，&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;max_tokens&quot;</span><span class="punctuation">:</span> <span class="number">50</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><code>render_chat_request</code> 就是把带 role 的 messages → 拼成模型认识的格式 → tokenize → <code>token_ids</code>。比如 Qwen 模型会拼成：</p><figure class="highlight smalltalk"><table><tr><td class="code"><pre><span class="line">&lt;|im_start|&gt;system</span><br><span class="line">你是一个助手&lt;|im_end|&gt;</span><br><span class="line">&lt;|im_start|&gt;user</span><br><span class="line">今天天气怎么样&lt;|im_end|&gt;</span><br><span class="line">&lt;|im_start|&gt;assistant</span><br></pre></td></tr></table></figure><p>这一步就是对话模板渲染，把 role+content 结构转成模型训练时见过的文本格式，然后再 tokenize。</p><hr><p>下面继续分析 这个适配器的create_chat_completion的迭代结果方法</p><p><mark style="background:#fff88f">实际上是调用的_create_chat_completion方法</mark></p><p><img src="/images/Pasted%20image%2020260807193902.png"></p><p><img src="/images/Pasted%20image%2020260807193920.png"></p><p><img src="/images/Pasted%20image%2020260807163934.png" alt="420"></p><p>可以看到，这个方法的逻辑是：</p><ul><li>获取分词器</li><li>request 转换模版</li><li>构建输出用的结构化解析器</li><li>prompt转成token ids</li><li>设置请求ID</li><li>开始获取本请求的prompt<ul><li>计算长度</li><li>计算生成长度</li><li>设置采样器参数</li><li>调用子类的generate来产生迭代器</li><li><img src="/images/Pasted%20image%2020260807205111.png"></li><li>收集我们这个子请求的迭代器，然后添加到我们这个大的请求的迭代器里面。</li><li>大多数时候一个请求就是一个子请求，也就是list.size &#x3D; 1</li><li><img src="/images/Pasted%20image%2020260807205324.png" alt="274"></li></ul></li><li><img src="/images/Pasted%20image%2020260807210237.png" alt="381"></li><li>这个夹在<strong>迭代器返回 - api_router的create_chat_completion之间</strong>，把迭代出来的<strong>RequestOutput转成str</strong></li><li>之后api_router的await返回的就是str的迭代器，进入StreamingResponse来async for实现真正的流式输出</li></ul><p><mark style="background:#d3f8b6">总结：</mark><br><img src="/images/Pasted%20image%2020260807210533.png"></p><p>至此，我们的&#x2F;v1&#x2F;chat&#x2F;completions的接口任务已经分析完成了</p><hr><p>init_app_state，也就是适配器层的对象都创建然后绑定到app了，接下来就是启动我们这个api server。</p><p><img src="/images/Pasted%20image%2020260807211142.png"></p><p>至此，我们这个在线服务api server的进程就完成了，一直会await等待shutdown_task，否则服务就会一直启动中。</p><p><img src="/images/Pasted%20image%2020260805232226.png"><br><img src="/images/Pasted%20image%2020260805232410.png"><br><img src="/images/Pasted%20image%2020260805233339.png"></p><p><img src="/images/Pasted%20image%2020260805233645.png"></p><p><img src="/images/Pasted%20image%2020260805234002.png"></p><h3 id="commit"><a href="#commit" class="headerlink" title="commit"></a>commit</h3><p><img src="/images/Pasted%20image%2020260805235359.png"></p><h2 id="2-引擎启动"><a href="#2-引擎启动" class="headerlink" title="2_引擎启动"></a>2_引擎启动</h2><p>这里主要分析引擎前端是如何在init的时候，启动引擎后端的，以及他们的进、线程的关系。</p><p>也就是engine_client这个变量的创建过程</p><p><img src="/images/Pasted%20image%2020260807211433.png"></p><p><img src="/images/Pasted%20image%2020260807211407.png"><br><img src="/images/Pasted%20image%2020260807211622.png"></p><p><img src="/images/Pasted%20image%2020260807211506.png"></p><p>总结一下这个过程</p><ul><li>build_async_engine_client -&gt; engine_client<ul><li>build_async_engine_client_from_engine_args 构造出async_llm的子类对象实例 yield + 上下文管理器抛出</li></ul></li><li>build_and_serve<ul><li>init_app_state<ul><li>state.engine_client &#x3D; engine_client 把这个async_llm的子类对象实例，绑定到app上面</li></ul></li></ul></li></ul><p>这就是引擎前端的调用过程。</p><hr><p>下面来具体分析async_llm这个引擎前端子类实例对象的构造过程，看看一个引擎前端进程是如何创建多个引擎后端进程的</p><p>先来简单看一下引擎后端启动的调用逻辑：</p><p><strong>完整启动链</strong></p><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line">AsyncLLM<span class="selector-class">.__init__</span>()</span><br><span class="line">  │  EngineCoreClient<span class="selector-class">.make_async_mp_client</span>() # 创建引擎传输层 + 引擎后端</span><br><span class="line">  ▼</span><br><span class="line">AsyncMPClient<span class="selector-class">.__init__</span>() → super()<span class="selector-class">.__init__</span>() → MPClient<span class="selector-class">.__init__</span>()</span><br><span class="line">  │  <span class="built_in">launch_core_engines</span>()</span><br><span class="line">  ▼</span><br><span class="line">CoreEngineProcManager<span class="selector-class">.__init__</span>()</span><br><span class="line">  │  multiprocessing<span class="selector-class">.Process</span>(target=EngineCoreProc.run_engine_core)</span><br><span class="line">  ▼</span><br><span class="line">新进程: EngineCoreProc                                   ← 真正的 GPU 推理进程</span><br><span class="line">  │  加载模型权重 → 创建 Scheduler → 创建 Executor → 死循环取请求做推理</span><br></pre></td></tr></table></figure><p>IPC 管道就是之前看的 ZMQ socket：两个进程之间通过 ZMQ ROUTER&#x2F;DEALER 收发 <code>EngineCoreRequest</code> 和 <code>EngineCoreOutputs</code>。</p><p><strong>这才是完整的 <code>self.engine_core</code>——不仅是一个 ZMQ 客户端，它同时还 fork 了一个跑模型的子进程。</strong></p><hr><p>先来展示一下，v1&#x2F;engine&#x2F;相关的主要用到的文件</p><p><img src="/images/Pasted%20image%2020260808184446.png"></p><hr><h4 id="代码梳理"><a href="#代码梳理" class="headerlink" title="代码梳理"></a>代码梳理</h4><p>下面将根据引擎前端进程的引擎前端对象，根据这个目录的文件，来逐步trace代码，看看是如何启动引擎后端，然后连接好，逐步工作的</p><p><mark style="background:#d3f8b6">async_llm.py</mark></p><p>前面一节，我们说到，在app的build_async_engine_client那个函数里面，构造了一个AsyncLLM类</p><p><img src="/images/Pasted%20image%2020260808230946.png" alt="561"></p><p><strong>这里分析一下类的继承关系</strong>：</p><ul><li>基类：EngineClient<ul><li>这个就是一个抽象基类，子类分为<strong>异步前端和同步前端</strong></li><li>里面的方法全是抽象方法</li></ul></li><li>子类<ul><li>AsyncLLM：<ul><li>这就是我们的异步的<strong>引擎前端</strong>类，下面是提供的一些方法：</li><li><img src="/images/Pasted%20image%2020260808231446.png"></li></ul></li></ul></li></ul><p>也就是说，我们的app在构造这个引擎前端的时候，是调用的<code>from_vllm_config</code>这个工厂函数来调用init方法构造类对象实例的。<br><img src="/images/Pasted%20image%2020260808231651.png" alt="453"></p><blockquote><p>这里的调用的是cls()的方法，关于这个的方法，主要还是方便继承的父类方法直接构造子类<br><img src="/images/Pasted%20image%2020260808231836.png"></p></blockquote><p>所以我这里调用的是 AsyncLLM子类的__init__的构造方法</p><p>下面来看一下我们的AsyncLLM的引擎前端对象实例，里面包含了那些东西<br><img src="/images/Pasted%20image%2020260808232459.png" alt="509"></p><p>可以看到，核心的就是：</p><ul><li>配置实例</li><li>输入预处理器实例</li><li>输入、输出格式转换器</li><li>self.engine_core<ul><li>注意，这里的这个不是单指的引擎后端，是包括前端进程里面剩下的所有设计后端管理的：<ul><li>ZMQ传输实例</li><li>引擎后端进程管理器实例</li></ul></li></ul></li><li>一个后台协程任务</li></ul><p>所以下面主要分析：</p><ul><li><mark style="background:#ff4d4f">self.engine_core</mark></li><li><mark style="background:#ff4d4f">后台协程  _run_output_handler </mark></li></ul><hr><p>先分析self.engine_core的构造吧<br><img src="/images/Pasted%20image%2020260808233342.png"></p><p>可以看到，调用的是<strong>EngineCoreClient</strong>的工厂函数</p><p><img src="/images/Pasted%20image%2020260808233440.png" alt="492"></p><p>而这个是一个基类，同样也是一个工厂函数</p><p><img src="/images/Pasted%20image%2020260808233510.png" alt="482"></p><p>所以他实际上，构造的是<mark style="background:#fff88f">AsyncMPClient</mark>这个类的对象，这个是一个<mark style="background:#fff88f">传输层的对象</mark></p><p>下面我们来看一下这个引擎传输层的类的继承关系</p><ul><li><mark style="background:#fff88f">基类：EngineCoreClient</mark><ul><li><img src="/images/Pasted%20image%2020260808233812.png" alt="394"></li><li>可以看到这个基类就是提供了<strong>工厂函数来构造不同的引擎传输层</strong>：</li><li><img src="/images/Pasted%20image%2020260808233853.png"></li><li><mark style="background:#fff88f">子类：MPClient</mark><ul><li><strong>这个类来fork我们真正的引擎后端进程管理器</strong></li><li><mark style="background:#fff88f">子类：AsyncMPClient</mark><ul><li>再增加前端引擎后端的输出缓存协程队列:<strong>output_queue</strong></li><li><img src="/images/Pasted%20image%2020260808234241.png"></li></ul></li></ul></li></ul></li></ul><blockquote><p>所以，EngineCoreClient基类，仅负责来选择构造什么样的引擎后端。</p><p>然后MPClient子类，来负责构造实际的引擎后端进程管理器，</p><p>然后在这个类的基础上，AsyncMPClient子类来构造专属于这个引擎后端进程的传输层。</p></blockquote><p>下面来看一下MPClient类是如何具体构造我们的引擎后端进程管理器的</p><ol><li>构建引擎前端的ZMQ的context（绑在了引擎传输层上，所以也是属于前端的一部分）：<br><img src="/images/Pasted%20image%2020260808234644.png"></li></ol><blockquote><p><img src="/images/Pasted%20image%2020260808234947.png"><br><mark style="background:#ff4d4f">重要</mark></p><p><strong>所以这个时候你在看，就是引擎前端实例，的self.engine_core这个成员，用来保存的是AsyncMPClient这个实例对象的，这个对象，包含隶属于引擎前端的传输层，也包含用于构造和管理引擎后端进程的引擎后端进程管理器</strong>。</p></blockquote><ol start="2"><li><p>构造一个资源管理器，用来统一回收加入的资源<br><img src="/images/Pasted%20image%2020260808234820.png"></p></li><li><p><mark style="background:#fff88f">这里就是开始创建好针对这个引擎前端的 ROUTER + PULL 套接字</mark></p><ol><li>然后把这些套接字都加入上面的资源管理器<br><img src="/images/Pasted%20image%2020260808235226.png"></li></ol></li></ol><p><mark style="background:#ff4d4f">重要</mark></p><p><strong>这里就是最精彩的部分，with as + launch_core_engines，就是开始真正创建我们的引擎后端进程的地方。</strong></p><p><img src="/images/Pasted%20image%2020260808235542.png" alt="595"></p><p>这个方法，可以看到被装饰成一个上下文管理器，因此，yield上部分是一个资源对象的构造逻辑，下部分是这个资源的等待配对&#x2F;销毁逻辑，写在一起了。</p><p>然后返回一个引擎后端进程管理器：<br><img src="/images/Pasted%20image%2020260808235853.png" alt="539"></p><blockquote><p><strong>可以看到yield上半部的逻辑是构建CoreEngineProcManager类的实例对象，然后直接抛出，等外面的with as 的逻辑走完了，在进入yield的下半部，进行等待引擎进程们启动</strong></p></blockquote><p><mark style="background:#ff4d4f">重要</mark></p><p>下面来分析一下看看 CoreEngineProcManager 这个引擎后端进程管理器是如何构造和启动引擎后端进程们的。</p><p>看一下__init__的输入参数：<br><img src="/images/Pasted%20image%2020260809000716.png" alt="610"></p><p>构造逻辑：<br><img src="/images/Pasted%20image%2020260809000735.png" alt="543"></p><p><img src="/images/Pasted%20image%2020260809000852.png" alt="514"></p><p><img src="/images/Pasted%20image%2020260809000916.png" alt="515"></p><p>至此，<mark style="background:#fff88f">我们的主进程：</mark></p><ul><li>引擎前端实例：class AsyncLLM<ul><li>成员属性engint_core &#x3D; EngineCoreClient.make_async_mp_client （基类工厂方法）-&gt; class AsyncMPClient<ul><li>基类：MPClient <ul><li>准备ROUTER + PULL 套接字</li><li>资源管理器，保存 引擎后端进程管理器对象 launch_core_engines<ul><li>构造 class CoreEngineProcManager 实例<ul><li><mark style="background:#fff88f">按照要求，proc.start() 拉起来配置需要的引擎后端进程</mark>，引擎后端进程的入口函数为：target&#x3D;EngineCoreProc.<mark style="background:#fff88f">run_engine_core</mark></li></ul></li><li>等待引擎进程连接</li></ul></li></ul></li><li>子类：AsyncMPClient<ul><li>提供output_queue 协程队列 + process_output_socket的后台协程任务</li></ul></li></ul></li></ul></li></ul><p>之后，我们的主进程就进入一些接力的后台接受输出的任务：<br><img src="/images/Pasted%20image%2020260809002421.png"></p><p><img src="/images/Pasted%20image%2020260809002507.png"></p><p>以上就是我们的主进程，也就是引擎前端进程。</p><p>下面来看看引擎后端进程的入口函数为：target&#x3D;EngineCoreProc.<mark style="background:#fff88f">run_engine_core</mark> 是如何具体进入的，调用的刚好是引擎后端进程类的一个工厂函数</p><p><strong>此时已经是一个新的进程了</strong>。可以看到这里的类关系是：</p><ul><li><mark style="background:#fff88f">基类：引擎后端类</mark><ul><li><mark style="background:#fff88f">子类：引擎后端进程类</mark></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260809002707.png" alt="535"></p><p><img src="/images/Pasted%20image%2020260809002721.png" alt="550"><br><img src="/images/Pasted%20image%2020260809003046.png" alt="542"><br><img src="/images/Pasted%20image%2020260809003141.png" alt="352"><br>依然是调用的子类的一个静态方法，来构造一个子类的实例，然后利用基类构造来创建内部的引擎后端</p><p>构造完我们的引擎后端进程类实例后，我们调用这个实例的一个开始工作循环，这个就是我们的这个<mark style="background:#fff88f">进程的主要逻辑</mark>。</p><p>我们先来看看构造子类和基类：<br><mark style="background:#fff88f">看看构造基类EngineCore</mark>, 这个其实已经变成了操作对象实例了，我们主要的进程逻辑在子类市里的run_busy_loop()里面</p><p><img src="/images/Pasted%20image%2020260809003814.png"></p><p>所以，我们看到的引擎后端：EngineCore实例，其实是一个独立的调度域</p><blockquote><p>所以<mark style="background:#fff88f">EngineCore是和一个完整模型的推理过程绑定的</mark>，</p><p>所以DP等于多少，EngineCore就是多少，</p><p>TP是一个模型推理的拆分，所以TP是多少，worker是多少</p><p>所以，<mark style="background:#fff88f">一个EngineCore， 是独立管理一块逻辑上的kv cache的</mark>，因为是和一个模型的完整推理需要的kvcache相关的</p></blockquote><p>简单总结一下EngineCore的构造逻辑：<br><img src="/images/Pasted%20image%2020260809005131.png"></p><p>这里不多分析，下面看看EngineCoreProc的子类实例的构造</p><p>这里的init部分，主要是：</p><ul><li>关于如何把前后端的进程进行握手，连接</li><li>创建输入输出线程：procress_input&#x2F;output_sockets</li></ul><p><img src="/images/Pasted%20image%2020260809005147.png"></p><p><img src="/images/Pasted%20image%2020260809005201.png"></p><p><mark style="background:#fff88f">至此，引擎前后端，都启动成功，引擎后端进程启动完成之后，就开始进入engine_core.run_busy_loop()，开始工作</mark></p><p><img src="/images/Pasted%20image%2020260809005522.png"></p><p><img src="/images/Pasted%20image%2020260809155939.png"></p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>用一张图来总结一下，各个类对象实例的包含关系，以及重要的数据结构</p><p><img src="/images/Pasted%20image%2020260809195716.png" alt="697"></p><h2 id="2-5-模块间通信格式梳理"><a href="#2-5-模块间通信格式梳理" class="headerlink" title="2.5_模块间通信格式梳理"></a>2.5_模块间通信格式梳理</h2><h2 id="3-引擎后端架构实现"><a href="#3-引擎后端架构实现" class="headerlink" title="3_引擎后端架构实现"></a>3_引擎后端架构实现</h2><h4 id="架构分析"><a href="#架构分析" class="headerlink" title="架构分析"></a>架构分析</h4><p>这部分其实就是EngineCoreProc的run_busy_loop的内容了，启动了的后端进程，里面都在干些什么呢？</p><p><img src="/images/Pasted%20image%2020260809200425.png"></p><p>可以看到，就干2件事：</p><ul><li>input_queue的请求，分发给scheduler</li><li>_process_engine_step(), 进行一次推理</li></ul><h5 id="分发请求到调度器-process-input-queue"><a href="#分发请求到调度器-process-input-queue" class="headerlink" title="分发请求到调度器_process_input_queue()"></a>分发请求到调度器_process_input_queue()</h5><p><img src="/images/Pasted%20image%2020260809200628.png"><br>主要核心就是每次看到input_queue里面有东西，就拿出来调用：</p><ul><li>_handle_client_request 来分发请求</li></ul><p><img src="/images/Pasted%20image%2020260809200721.png"></p><p>具体的分发逻辑，是按照收到的请求的指令类型来调用不同的调度器接口</p><p><img src="/images/Pasted%20image%2020260809200800.png"></p><p>所以不存在”多个分发队列”——队列全部在 Scheduler 类内部：</p><table><thead><tr><th>队列</th><th>位置</th><th>作用</th></tr></thead><tbody><tr><td><code>input_queue</code></td><td>EngineCoreProc</td><td>ZMQ → 主循环的中转站（你说得对）</td></tr><tr><td><code>self.waiting</code></td><td>Scheduler</td><td>新请求排队，等 <code>schedule()</code> 选中</td></tr><tr><td><code>self.running</code></td><td>Scheduler</td><td>正在跑的请求，每 step 重新调度</td></tr><tr><td><code>finished list</code></td><td>Scheduler</td><td>本步完成&#x2F;中止的，下步清理 cache</td></tr></tbody></table><p><code>_handle_client_request</code> 只是一个<strong>路由器</strong>，不存数据。</p><ul><li>ADD → <code>scheduler.add_request()</code> 塞进 waiting 队列</li><li>ABORT → <code>scheduler.finish_requests()</code> 标记完成。</li></ul><p>真正的调度逻辑在 <code>schedule()</code> 里：从 <code>waiting</code> 取、分配 KV cache、放进 <code>running</code>。</p><hr><h5 id="执行一步推理-process-engine-step"><a href="#执行一步推理-process-engine-step" class="headerlink" title="执行一步推理_process_engine_step"></a>执行一步推理_process_engine_step</h5><p><img src="/images/Pasted%20image%2020260809201917.png"></p><p>调用注册的step()方法，来让基类EngineCore来驱动Scheduler 和 Executor 来完成一个token的推理，</p><p><img src="/images/Pasted%20image%2020260809202925.png"></p><p>然后把结果放到output_queue里面，之后返回这次的状态，到底有没有执行</p><h3 id="3-1-调度器scheduler"><a href="#3-1-调度器scheduler" class="headerlink" title="3.1_调度器scheduler"></a>3.1_调度器scheduler</h3><p>下面来分析调度器，是如何根据自己内部的各种队列，来实现调度策略的</p><p>我自己总结一下调度器类在构造时的基础属性</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Scheduler</span>():</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">vllm_config <span class="comment"># 推理框架配置</span></span><br><span class="line">scheduler_config <span class="comment"># 调度器配置</span></span><br><span class="line">cache_config <span class="comment"># kvcache的配置</span></span><br><span class="line">kv_cache_config</span><br><span class="line">parallel_config <span class="comment"># 并行化配置（没什么用）</span></span><br><span class="line"></span><br><span class="line">max_num_running_reqs <span class="comment"># 每轮最大req数</span></span><br><span class="line">max_num_scheduled_tokens <span class="comment"># 每轮最大计算token数</span></span><br><span class="line">max_model_len <span class="comment"># 单个req的模型kvcache上下文窗口长度</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">num_gpu_blocks <span class="comment"># GPU的block pool的大小</span></span><br><span class="line"></span><br><span class="line">requests : <span class="built_in">dict</span>[<span class="built_in">str</span>, Request] <span class="comment"># 保存所有历史请求</span></span><br><span class="line"></span><br><span class="line">policy <span class="comment"># 调度策略，FCFS还是PRIORITY，决定req的挑选顺序</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 调度的主要的3个队列</span></span><br><span class="line"><span class="variable language_">self</span>.waiting <span class="comment"># 遵循policy的优先级队列</span></span><br><span class="line"><span class="variable language_">self</span>.skipped_waiting <span class="comment"># 遵循policy的优先级队列</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.running : <span class="built_in">list</span>[Request]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.hash_block_size <span class="comment"># 每个hash块多少个token数</span></span><br><span class="line"><span class="variable language_">self</span>.kv_cache_manager <span class="comment"># kvcache的block管理器</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">current_step = <span class="number">0</span> <span class="comment"># 步进计数</span></span><br><span class="line">_pause_state <span class="comment"># 调度器状态</span></span><br><span class="line"></span><br><span class="line">_inflight_prefills <span class="comment"># 正在prefill阶段的req的集合</span></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"></span><br></pre></td></tr></table></figure><p>先来看一下调度器类的定义：</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line"> === 类说明 ===</span><br><span class="line">    继承: SchedulerInterface(ABC)</span><br><span class="line">    职责: vLLM V1 核心调度器。管理请求生命周期（等待→运行→完成），</span><br><span class="line">          分配 KV cache block，每步选取 batch 交给模型执行器做前向推理。</span><br><span class="line"></span><br><span class="line">=== 基类方法实现 (22个) ===</span><br><span class="line">    schedule()              — 核心：选取请求 + 分配 KV cache</span><br><span class="line">    update_from_output()    — 根据模型输出更新请求状态，返回 EngineCoreOutputs</span><br><span class="line">    add_request()           — 新请求加入等待队列</span><br><span class="line">    finish_requests()       — 中止/停止请求</span><br><span class="line">    get_num_unfinished_requests() — 未完成请求数</span><br><span class="line">    has_unfinished_requests()     — 是否有未完成请求</span><br><span class="line">    has_finished_requests()       — 是否有已完成待清理的请求</span><br><span class="line">    has_requests()          — 是否有任何待处理请求</span><br><span class="line">    update_draft_token_ids()      — 更新草稿 token（投机解码）</span><br><span class="line">    update_draft_token_ids_in_output() — 同上 + 更新 SchedulerOutput</span><br><span class="line">    get_grammar_bitmask()   — 获取结构化输出语法掩码</span><br><span class="line">    get_request_counts()    — 返回 (运行中, 等待中) 数量</span><br><span class="line">    get_kv_cache_usage()    — KV cache 使用率</span><br><span class="line">    pause_state / set_pause_state — 暂停状态</span><br><span class="line">    reset_prefix_cache()    — 重置 KV 前缀缓存</span><br><span class="line">    reset_encoder_cache()   — 重置编码器缓存</span><br><span class="line">    make_stats()            — 生成调度统计</span><br><span class="line">    shutdown()              — 关闭</span><br><span class="line">    get_kv_connector() / get_ec_connector() / get_kv_event_publisher_config()</span><br><span class="line"></span><br><span class="line">=== [新增] 公有方法 (2个) ===</span><br><span class="line">    reset_connector_cache()     — 重置 KV connector 缓存 (disaggregated prefill)</span><br><span class="line">    make_spec_decoding_stats()  — 生成投机解码统计</span><br><span class="line"></span><br><span class="line">=== [新增] 核心成员属性 ===</span><br><span class="line">    —— 请求容器 ——</span><br><span class="line">        requests: dict[str, Request]    — 所有请求 &#123;req_id → Request&#125;</span><br><span class="line">        waiting: RequestQueue           — 等待队列（按策略排队）</span><br><span class="line">        skipped_waiting: RequestQueue   — 因依赖/约束被跳过的请求</span><br><span class="line">        running: list[Request]          — 正在运行的请求列表</span><br><span class="line">        finished_req_ids: set[str]      — 本步完成的请求 ID</span><br><span class="line">    —— 调度约束 ——</span><br><span class="line">        max_num_running_reqs: int       — 最大并发请求数</span><br><span class="line">        max_num_scheduled_tokens: int   — 每步最大调度 token 数</span><br><span class="line">        max_model_len: int              — 模型最大上下文长度</span><br><span class="line">        block_size: int                 — KV cache block 大小</span><br><span class="line">        current_step: int               — 调度步数计数器</span><br><span class="line">        policy: SchedulingPolicy        — 调度策略 (FCFS/Priority)</span><br><span class="line">    —— KV Cache ——</span><br><span class="line">        kv_cache_manager: KVCacheManager — KV cache 管理器（block 分配/回收）</span><br><span class="line">    —— 投机解码 ——</span><br><span class="line">        use_eagle: bool                 — 是否使用 EAGLE 投机解码</span><br><span class="line">        num_spec_tokens: int            — 投机 token 数</span><br><span class="line">        num_lookahead_tokens: int       — 前瞻 token 数</span><br><span class="line">    —— KV 传输 (disaggregated prefill) ——</span><br><span class="line">        connector: KVConnectorBase_V1   — KV connector（P/D 分离场景）</span><br><span class="line">        ec_connector: ECConnectorBase   — EC connector（弹性扩容）</span><br><span class="line">    —— 多模态 ——</span><br><span class="line">        encoder_cache_manager           — 编码器缓存管理器</span><br><span class="line">        max_num_encoder_input_tokens    — 编码器最大输入 token 数</span><br><span class="line">    —— 状态追踪 ——</span><br><span class="line">        sched_step_seq / processed_step_seq — 调度/处理步序号</span><br><span class="line">        deferred_frees: deque           — 延迟释放的 block 队列</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>先来同步一下一些概念：<br><img src="/images/Pasted%20image%2020260809202434.png"><br><img src="/images/Pasted%20image%2020260809202513.png"></p><p>基本上就是这个类比，类似linux的进程设计。状态流转也类似：</p><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line">                 schedule() 选中</span><br><span class="line">waiting ───────────────────────────────► <span class="built_in">running</span></span><br><span class="line">(就绪)                                    (运行)</span><br><span class="line"></span><br><span class="line"><span class="built_in">running</span> ─── 被抢占 ───────────────────► waiting</span><br><span class="line">          ─── 完成/中止 ──────────────► finished</span><br><span class="line"></span><br><span class="line">waiting ─── 条件不满足 ────────────────► skipped_waiting</span><br><span class="line">(就绪)     (等远程KV/超LoRA上限/...)     (阻塞)</span><br><span class="line"></span><br><span class="line">skipped_waiting ─── 条件满足 ─────────► waiting → <span class="built_in">running</span></span><br><span class="line">(阻塞)            (下轮schedule重试)</span><br></pre></td></tr></table></figure><p>和 Linux 进程调度确实很像：<code>running</code> &#x3D; <code>TASK_RUNNING</code>（在 CPU 上），<code>waiting</code> &#x3D; runqueue 里排队的，<code>skipped_waiting</code> &#x3D; <code>TASK_UNINTERRUPTIBLE</code> 这种等 I&#x2F;O 的阻塞态——等远程 KV 传完、等 LoRA slot 空出来、等多模态编码完。每轮 <code>schedule()</code> 都会把 <code>skipped_waiting</code> 里的请求重新拿出来看一眼，条件满足了就恢复。</p><hr><h4 id="kvcachemanager"><a href="#kvcachemanager" class="headerlink" title="kvcachemanager"></a>kvcachemanager</h4><ul><li>KVCacheManager 是 Scheduler 和底层 KV cache 之间的抽象层</li><li>KV cache 以 <strong>block</strong> 为单位分配（不是逐 token 分配），一个 block 存储连续若干个 token 的 KV 值</li></ul><table><thead><tr><th>参数</th><th>含义</th><th>单 group 时</th><th>多 group 时</th></tr></thead><tbody><tr><td><code>scheduler_block_size</code></td><td>调度分配的<strong>对齐粒度</strong></td><td><code>block_size * DCP</code> (如 16)</td><td>所有 group 的 <strong>LCM</strong>（最小公倍数）</td></tr><tr><td><code>hash_block_size</code></td><td>prefix cache <strong>哈希匹配粒度</strong></td><td>&#x3D; scheduler_block_size</td><td>所有 group 的 <strong>GCD</strong>（最大公约数）</td></tr></tbody></table><p>为什么需要两个不同的值？混合架构模型（如 Jamba，attention + Mamba）：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">attention</span> group: block_size = <span class="number">16</span></span><br><span class="line"><span class="attribute">Mamba</span> group:     block_size = <span class="number">64</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">scheduler_block_size</span> = LCM(<span class="number">16</span>, <span class="number">64</span>) = <span class="number">64</span>   ← 分配和计算对齐用 <span class="number">64</span> token</span><br><span class="line"><span class="attribute">hash_block_size</span>      = GCD(<span class="number">16</span>, <span class="number">64</span>) = <span class="number">16</span>   ← prefix 匹配用 <span class="number">16</span> token（更细）</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="variable language_">self</span>.kv_cache_manager = KVCacheManager(...)</span><br><span class="line">    ├── coordinator        ← 管理实际的 KVCacheBlock 池（分配/释放/引用计数）</span><br><span class="line">    ├── block_pool         ← block 池引用</span><br><span class="line">    ├── empty_kv_cache_blocks  ← 预构造的空 block 对象（避免 GC）</span><br><span class="line">    ├── watermark_blocks   ← 保留 N 个自由 block，避免频繁抢占</span><br><span class="line">    └── prefix_cache_stats ← prefix cache 命中率统计</span><br></pre></td></tr></table></figure><ul><li>Scheduler 调用 <code>allocate_slots()</code> </li><li>→ KVCacheManager 算需要多少 block </li><li>→ Block Pool 分配 </li><li>→ 返回 <code>KVCacheBlocks</code>。</li><li></li><li>如果 pool 不够了，返回 None，Scheduler 就触发抢占。</li><li></li></ul><p>这就是之前 <code>schedule()</code> 里 <code>new_blocks is None → break / preempt</code> 那一段。</p><h4 id="prefix-cache"><a href="#prefix-cache" class="headerlink" title="prefix cache"></a>prefix cache</h4><p>前缀缓存就是：<strong>两个请求共享了开头的 token，第二个请求的 KV 值不用重算，直接复用</strong><br><img src="/images/Pasted%20image%2020260809221411.png"></p><p><img src="/images/Pasted%20image%2020260809221504.png"></p><p>关于group的含义，我们这里先专注于Attention架构的LLM，所以就是一个group<br><img src="/images/Pasted%20image%2020260809221724.png"></p><p>这就是 kv cache block的设计的作用<br><img src="/images/Pasted%20image%2020260809222006.png"><br><img src="/images/Pasted%20image%2020260809222123.png"></p><p>这就是kvcachemanager的虚拟页表的设计思想，实际上就是kvcache的显存管理器<br><img src="/images/Pasted%20image%2020260809222308.png"><br><img src="/images/Pasted%20image%2020260809222439.png"></p><p><img src="/images/Pasted%20image%2020260809222517.png"></p><p>整个初始化流程：</p><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line">EngineCore<span class="selector-class">.__init__</span>()</span><br><span class="line">  │</span><br><span class="line">  ├── model_executor<span class="selector-class">.determine_available_memory</span>()   ← 跑一遍算模型峰值显存，确定 KV cache 可用多少</span><br><span class="line">  │</span><br><span class="line">  ├── <span class="built_in">get_kv_cache_configs</span>(available_memory)        ← 算出来: block_size=<span class="number">16</span>, num_blocks=<span class="number">5000</span></span><br><span class="line">  │</span><br><span class="line">  ├── model_executor.<span class="built_in">initialize_from_config</span>(...)    ← Worker 在 GPU 上真正把显存分配掉</span><br><span class="line">  │</span><br><span class="line">  └── <span class="built_in">KVCacheManager</span>(kv_cache_config, ...)          ← 拿到 block_pool: <span class="number">5000</span> 个物理 block</span><br></pre></td></tr></table></figure><p>之后 KVCacheManager 不碰 GPU 显存分配了——它管的是这 5000 个 block 的<strong>逻辑账本</strong>（谁在用、引用计数多少、哪些空闲）。真正的 GPU 显存由 Worker 侧的 Block Pool 持有，初始化完就不再扩了。</p><p>关键的分工是：</p><p><strong>申请 block 的时机在 Scheduler，不在 Executor。</strong></p><figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line"><span class="keyword">Scheduler </span>(CPU)                              Executor (GPU Worker)</span><br><span class="line">─────────────                                ──────────────────────</span><br><span class="line"><span class="keyword">schedule():</span></span><br><span class="line"><span class="keyword"></span>  KVCacheManager.allocate_slots()</span><br><span class="line">    → 返回 new_blocks                         </span><br><span class="line">    → 放进 <span class="keyword">SchedulerOutput </span>                   </span><br><span class="line">      ├── <span class="keyword">scheduled_new_reqs </span>                 收到 <span class="keyword">SchedulerOutput</span></span><br><span class="line"><span class="keyword"></span>      ├── num_scheduled_tokens                知道每个请求的 <span class="keyword">block </span>映射</span><br><span class="line">      └── req_to_new_blocks ────────────────► 直接写 KV 到对应 <span class="keyword">block</span></span><br><span class="line"><span class="keyword"></span>                                                 ├── 复用 <span class="keyword">block </span>→ COW</span><br><span class="line">                                                 ├── 新 <span class="keyword">block </span>→ 填入 KV 值</span><br><span class="line">                                                 └── 完成 <span class="keyword">block </span>→ 更新引用</span><br></pre></td></tr></table></figure><p>Executor 只负责<strong>写入</strong>——Scheduler 说”请求 A 的 token 16-31 写到物理 block #42”，Executor 就算出 KV 往里填。Executor 自己不用跟 KVCacheManager 申请任何东西，block 分配在 <code>schedule()</code> 这一步已经全部做完了。</p><p>KVCacheManager 跑的是<strong>逻辑账本</strong>（谁用哪个 block、引用计数），Worker 侧跑的是<strong>物理仓库</strong>（GPU 显存里的真数据）。Scheduler 管账，Executor 只管搬货。</p><p><mark style="background:#ff4d4f">问答</mark></p><blockquote><p>等一下，你是说kvcachemanager里面的block pool 只是一个按block id来索引的列表，每个block id对应的显存空间，其实是所有层的这个位置的k,v cache的block显存空间，比如32层，那就是32层x2（kv）x16个token的大小。</p><p>然后你现在来的token，在调度器的时候，根据kvcachemanager，就知道他的block ID了，（这是因为attention每层的的kvcache顺序都是一样的，一个token在layer0写入12号block,在所有层的kvcache都写入第12号block对吧） </p><p>然后worker拿到这个token 以及他的block 号，来去每一层的对应的block位置去写是吗？ </p><p>可是我还有一个疑问，<mark style="background:#fff88f">这个block内，该token是16个中的第几个，怎么知道的</mark>？</p></blockquote><p>完全正确。第一个问题不重复了，直接答第三个：</p><p><strong>位置由 <code>num_computed_tokens</code> 推算出来。</strong></p><figure class="highlight tap"><table><tr><td class="code"><pre><span class="line">请求A: num_computed_tokens = 100</span><br><span class="line">       新 token 编号 = 100</span><br><span class="line">      <span class="number"> 100 </span>//<span class="number"> 16 </span>= block <span class="comment">#6</span></span><br><span class="line">      <span class="number"> 100 </span>%<span class="number"> 16 </span> = 位置 4（block 内第<span class="number"> 5 </span>个槽）</span><br><span class="line"></span><br><span class="line">请求B: num_computed_tokens = 47</span><br><span class="line">       新 token 编号 = 47</span><br><span class="line">      <span class="number"> 47 </span>//<span class="number"> 16 </span>= block <span class="comment">#2</span></span><br><span class="line">      <span class="number"> 47 </span>%<span class="number"> 16 </span> = 位置 15（block 内第<span class="number"> 15 </span>个槽，马上就满了）</span><br></pre></td></tr></table></figure><p>Worker 拿到 SchedulerOutput 里每个请求的三个信息就够了：</p><table><thead><tr><th>信息</th><th>作用</th></tr></thead><tbody><tr><td><code>num_computed_tokens</code></td><td>知道从哪个 token 位置开始写</td></tr><tr><td><code>num_scheduled_tokens</code></td><td>知道要写多少个 token</td></tr><tr><td><code>req_to_new_blocks</code> (block 列表)</td><td>知道写到哪个物理 block 里</td></tr></tbody></table><p>写第 n 个 token 时：<code>block_idx = (num_computed_tokens + n) // 16</code>，<code>slot = (num_computed_tokens + n) % 16</code>，找到 <code>blocks[block_idx]</code> → 拿到 block_id → <code>layer_k[block_id, slot, :, :]</code>。</p><p><img src="/images/Pasted%20image%2020260809230719.png" alt="512"></p><p>同时每个请求内部，都要维护一个自己请求占用的block的表，</p><p><img src="/images/Pasted%20image%2020260809231030.png"></p><p><mark style="background:#fff88f">每个请求维护自己的虚拟页表（block 列表），映射到物理 block pool 上。和操作系统给每个进程维护页表一个道理</mark></p><p>所以调度器负责选出执行哪个请求，得到需要新计算多少个token, 里面的kvcachemanager负责在这个请求里面附上这个请求的页表（block占用表），然后计算出新的token要写入的block。</p><p>worker收到这个请求后，根据token的序号，以及这个页表就知道，往哪里写入了。</p><p><img src="/images/Pasted%20image%2020260809231335.png"></p><p><img src="/images/Pasted%20image%2020260809231646.png"></p><p><img src="/images/Pasted%20image%2020260809231801.png"></p><h4 id="优化总结：pagedattention-blockmanager-prefix-cache"><a href="#优化总结：pagedattention-blockmanager-prefix-cache" class="headerlink" title="优化总结：pagedattention &#x3D; blockmanager + prefix cache"></a>优化总结：pagedattention &#x3D; blockmanager + prefix cache</h4><p>BlockPool + Prefix Cache &#x3D; PagedAttention 的两个核心支柱：</p><table><thead><tr><th>支柱</th><th>对应</th><th>解决的问题</th></tr></thead><tbody><tr><td><strong>分页管理</strong></td><td>BlockPool + 引用计数</td><td>显存不按请求预留，按 block 粒度动态分配&#x2F;回收</td></tr><tr><td><strong>页共享</strong></td><td>Prefix Cache + COW</td><td>共享前缀复用同一页，零拷贝零重算</td></tr></tbody></table><p>就是操作系统的虚拟内存思想搬到 KV cache 上——</p><ul><li>页表（每请求的 block 列表）、</li><li>物理页池（BlockPool）、</li><li>写时复制（COW）、</li><li>页回收（抢占&#x2F;evict），</li></ul><p>全套搬过来了。vLLM 论文的核心贡献就是这个。</p><p><img src="/images/Pasted%20image%2020260809232203.png"></p><h4 id="prefill-chunked"><a href="#prefill-chunked" class="headerlink" title="prefill chunked"></a>prefill chunked</h4><p><strong>Chunked Prefill</strong> 依赖 block 粒度分配——每个 chunk 只需分配自己的 block，下一个 chunk 分配新的 block。如果 KV cache 是连续分配的，一个长 prompt 必须一次性预留整段连续显存，切块就没有意义。</p><p><img src="/images/Pasted%20image%2020260809232317.png"></p><p>代码中三个关键逻辑：</p><p><strong>1. 确定单块大小</strong>（<a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/sched/scheduler.py#L971">line 971-973</a>）：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">threshold = <span class="variable language_">self</span>.scheduler_config.long_prefill_token_threshold  <span class="comment"># 比如 512</span></span><br><span class="line"><span class="keyword">if</span> <span class="number">0</span> &lt; threshold &lt; num_new_tokens:</span><br><span class="line">    num_new_tokens = threshold  <span class="comment"># 一刀切，最多 512 token/步</span></span><br></pre></td></tr></table></figure><p><strong>2. chunked prefill 没开时，长 prompt 装不下就直接不给调</strong>（<a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/sched/scheduler.py#L977">line 977-983</a>）：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.scheduler_config.enable_chunked_prefill <span class="keyword">and</span> num_new_tokens &gt; token_budget:</span><br><span class="line">    <span class="keyword">break</span>  <span class="comment"># 不调度这个请求，跳过</span></span><br></pre></td></tr></table></figure><p>开了之后，即使 prompt 有 4096 token，也只分配 <code>min(512, token_budget)</code> 个 token 的 block——剩下的下步再说。</p><p><strong>3. 请求保持 running 状态</strong>（<a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/sched/scheduler.py#L1146">line 1153</a>）：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 被 chunked 的请求 → 设置为 RUNNING，但不标记为 prefill_chunk</span></span><br><span class="line"><span class="comment"># 下一轮 scheduler 遍历 running 队列时发现它 num_computed_tokens &lt; num_tokens</span></span><br><span class="line"><span class="comment"># 就继续给它分配下一个 chunk</span></span><br><span class="line">request.status = RequestStatus.RUNNING</span><br></pre></td></tr></table></figure><p><strong>核心效果</strong>：长 prompt 的 TTFT 大幅改善——不用等 4096 token 全部 prefill 完才输出第一个 token，像 decode 一样穿插在每步中逐步推进。同时 PagedAttention 的 block 粒度分配让每个 chunk 独立分配 block，不要求连续显存。</p><p><strong>第一点</strong>：chunked prefill 解决的就是长 prompt 独占计算资源的问题。没有它，4096 token 的 prefill 一次吃掉全部 <code>token_budget</code>（8192 能装下），同 batch 的其他 decode 请求全被挤到下一步。</p><p><strong>第二点</strong>：对，<code>token_budget</code>（计算预算）和 KV cache 剩余（存储预算）是两套独立约束：</p><p><mark style="background:#fff88f">所以，chunked prefill解决的就是过长的prompt请求，独占调度位的问题。</mark></p><p><img src="/images/Pasted%20image%2020260809232922.png"></p><p>剩下的复杂度不在调度侧，而是在 <strong>Worker 执行侧</strong>：一个 batch 里同时有 prefill chunk（可能 512 token）和 decode（1 token），attention kernel 要处理两个不同长度的 KV 写入——decode 只写 1 个位置而 prefill chunk 要写 512 个。这个 ragged batch 的处理是 CUDA kernel 的活，不是调度器的活。</p><h4 id="continuous-batching"><a href="#continuous-batching" class="headerlink" title="continuous batching"></a>continuous batching</h4><p><strong>Continuous Batching</strong> 依赖 block 级别的动态混合——一个 batch 里 5 个 decode 请求各写 1 个 token，1 个 prefill 请求写 200 个 token，它们的 block 交错分配，互不干扰。没有分页管理，decode 和 prefill 的显存混在一起，新请求插不进来</p><p>Continuous Batching 的核心思想一句话：<strong>prefill 和 decode 没有物理隔离，每一步用一个 <code>token_budget</code> 混着跑</strong></p><p><img src="/images/Pasted%20image%2020260809234219.png"></p><p><img src="/images/Pasted%20image%2020260809234248.png"><br>所以”batch”没有消失，它只是变成了<strong>每步动态重新构成的临时集合</strong>。Scheduler 每次 <code>schedule()</code> 都从头遍历 <code>running</code> + <code>waiting</code>，重新决定这步谁上谁下。不像传统做法”锁死一批请求直到全部跑完”。</p><p>这就是为什么 <code>SchedulerOutput</code> 同时包含 <code>scheduled_new_reqs</code>（新进来的）、<code>scheduled_resumed_reqs</code>（恢复的）、<code>finished_req_ids</code>（这步完成的）、<code>preempted_req_ids</code>（这步踢掉的）——每一步 batch 的边界都可以变化。</p><h4 id="投机解码-speculative-decode"><a href="#投机解码-speculative-decode" class="headerlink" title="投机解码 speculative decode"></a>投机解码 speculative decode</h4><p>这个的思路是借鉴prefill，以及训练的时候是并行计算的，但是推理是串行推理。</p><p>所以，他们提出一种思路：<br><img src="/images/Pasted%20image%2020260810103530.png"></p><p>投机解码：</p><ul><li>大模型，小模型输入一个token</li><li>同步运行：<ul><li>大模型推理一步：y2’</li><li>小模型推理N步: y2’’, y3’’, y4’’, ….</li></ul></li><li>大模型验证小模型的草稿y2’’ 是否和自己的正确答案y2’，是一个分布, <mark style="background:#fff88f">小模型y2’’预测可信</mark><ul><li>大模型并行推理验证后面的<ul><li>大模型输入y2’’, y3’’, y4’’, ……, 得到y3^, y4^, y5^（大模型的标准答案）</li><li>比较：<ul><li>y3’’ 和 y3^ 验证同分布，采用</li><li>y4’’ 和 y4^ 验证不是同分布，丢弃</li><li>丢弃</li><li>丢弃</li><li>….</li></ul></li><li>一直到小模型的预测草稿出现错误，就不再采用，直接丢弃</li><li>最终大模型采用y2’’, y3’’ 作为大模型的输出。</li></ul></li></ul></li></ul><blockquote><p>一次大串行+多次小串行并行 ， 一次大并行评估分布，采用连续正确</p></blockquote><hr><h4 id="同步调度器-调度逻辑"><a href="#同步调度器-调度逻辑" class="headerlink" title="同步调度器-调度逻辑"></a>同步调度器-调度逻辑</h4><p><img src="/images/Pasted%20image%2020260810153127.png"></p><p>引擎的 <code>step()</code> 方法就是这三个动作的无限循环：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># core.py:647</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="comment"># 1. 调度器发出任务</span></span><br><span class="line">    scheduler_output = <span class="variable language_">self</span>.scheduler.schedule()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 2. GPU 执行</span></span><br><span class="line">    model_output = <span class="variable language_">self</span>.model_executor.execute_model(scheduler_output)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 3. 调度器用结果更新状态</span></span><br><span class="line">    engine_core_outputs = <span class="variable language_">self</span>.scheduler.update_from_output(</span><br><span class="line">        scheduler_output, model_output</span><br><span class="line">    )</span><br></pre></td></tr></table></figure><p>二者的分工</p><figure class="highlight gauss"><table><tr><td class="code"><pre><span class="line">                  <span class="built_in">schedule</span>()                       <span class="built_in">update_from_output</span>()</span><br><span class="line">                  ──────────                       ───────────────────</span><br><span class="line">操作对象:     Request 的选择 + KV cache 分配       GPU 返回的 <span class="built_in">token</span> + 计数器核销</span><br><span class="line"></span><br><span class="line">对谁做决策:    本轮要算哪些 request、算几个 <span class="built_in">token</span>     产出 <span class="built_in">token</span> 是否触发 <span class="keyword">stop</span>、</span><br><span class="line">                                                    spec <span class="built_in">token</span> 是否被拒绝</span><br><span class="line"></span><br><span class="line">计数器方向:    乐观递增 (赊账)                      核销/回滚 (平账)</span><br><span class="line">              computed ↑, in_flight ↑,             in_flight ↓, <span class="keyword">output</span> ↑,</span><br><span class="line">              placeholder ↑ (异步)                  placeholder ↓ (异步),</span><br><span class="line">                                                    computed ↓ (spec 拒绝时回滚)</span><br><span class="line"></span><br><span class="line">输出:          SchedulerOutput                      <span class="built_in">EngineCoreOutputs</span></span><br><span class="line">              (发给 GPU 的指令)                      (发给前端的 <span class="built_in">token</span> 流)</span><br></pre></td></tr></table></figure><p>调度器的内部状态（8 个计数器 + KV cache + request 队列）只在这两个方法中被修改，形成闭环：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line">┌──────────────────────────────────────────────────────┐</span><br><span class="line">│                    Scheduler 状态                      │</span><br><span class="line">│                                                      │</span><br><span class="line">│  running<span class="selector-attr">[]</span>  waiting<span class="selector-attr">[]</span>  requests&#123;&#125;  kv_cache_manager  │</span><br><span class="line">│                                                      │</span><br><span class="line">│  request<span class="selector-class">.num_computed_tokens</span>                         │</span><br><span class="line">│  request<span class="selector-class">.num_in_flight_tokens</span>                        │</span><br><span class="line">│  request<span class="selector-class">.num_output_placeholders</span>                     │</span><br><span class="line">│  request<span class="selector-class">.num_tokens_with_spec</span>                        │</span><br><span class="line">│  ...                                                 │</span><br><span class="line">└────────────┬──────────────────────┬──────────────────┘</span><br><span class="line">             │                      │</span><br><span class="line">       ① <span class="built_in">schedule</span>()          ③ <span class="built_in">update_from_output</span>()</span><br><span class="line">       发出本轮指令              用 GPU 结果更新状态</span><br><span class="line">             │                      │</span><br><span class="line">             ▼                      ▲</span><br><span class="line">        SchedulerOutput             │</span><br><span class="line">             │                      │</span><br><span class="line">             └──────② GPU ──────────┘</span><br><span class="line">                 <span class="built_in">execute_model</span>()</span><br></pre></td></tr></table></figure><blockquote><p><strong><code>schedule()</code> &#x3D; 向前看，预测下一步需要什么</strong><br><strong><code>update_from_output()</code> &#x3D; 回头看，把 GPU 实际产出的结果落盘到状态里</strong></p></blockquote><p>两步合作，缺一不可：没有 <code>schedule()</code> 状态无法推进，没有 <code>update_from_output()</code> 状态无人校验。这就是 vLLM V1 调度器的全部状态机逻辑。</p><h4 id="schedule-完整流程"><a href="#schedule-完整流程" class="headerlink" title="schedule() 完整流程"></a>schedule() 完整流程</h4><figure class="highlight arcade"><table><tr><td class="code"><pre><span class="line">schedule() 被 EngineCore.step() 调用</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">0</span>: 初始化</span><br><span class="line">│   token_budget = self.max_num_scheduled_tokens</span><br><span class="line">│   scheduled_new_reqs / resumed / running / preempted = []</span><br><span class="line">│   req_to_new_blocks = &#123;&#125;      ← 请求→本轮新分配的 block 映射</span><br><span class="line">│   num_scheduled_tokens = &#123;&#125;   ← 请求→本轮调度 token 数映射</span><br><span class="line">│   defer_prefills = throttle_prefills and not capacity_bound</span><br><span class="line">│                    and (running 中有 <span class="built_in">decode</span> 请求)</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">1</span>: RUNNING 遍历 (<span class="keyword">while</span> req_index &lt; len(running) and token_budget &gt; <span class="number">0</span>)</span><br><span class="line">│   │</span><br><span class="line">│   │  request = self.running[req_index]</span><br><span class="line">│   │</span><br><span class="line">│   ├── 跳过判断 ──────────────────────────────</span><br><span class="line">│   │   ├─ num_output_placeholders &gt; <span class="number">0</span> 且已满 → req_index++, <span class="keyword">continue</span></span><br><span class="line">│   │   ├─ current_step &lt; next_decode_eligible_step → req_index++, <span class="keyword">continue</span></span><br><span class="line">│   │   └─ defer_prefills 且这是 prefill chunk → req_index++, <span class="keyword">continue</span></span><br><span class="line">│   │</span><br><span class="line">│   ├── ① 计算 num_new_tokens ──────────────────</span><br><span class="line">│   │   num_new_tokens = num_tokens_with_spec + num_output_placeholders</span><br><span class="line">│   │                     - num_computed_tokens</span><br><span class="line">│   │   ├─ long_prefill_token_threshold 截断  (chunked prefill)</span><br><span class="line">│   │   ├─ <span class="built_in">min</span>(num_new_tokens, token_budget)</span><br><span class="line">│   │   └─ <span class="built_in">min</span>(num_new_tokens, max_model_len - num_computed_tokens - <span class="number">1</span>)</span><br><span class="line">│   │</span><br><span class="line">│   ├── num_new_tokens == <span class="number">0</span>? → req_index++, <span class="keyword">continue</span>  (不严格FCFS)</span><br><span class="line">│   │</span><br><span class="line">│   ├── ② allocate_slots() ────────────────────</span><br><span class="line">│   │   │</span><br><span class="line">│   │   │  <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">│   │   │    new_blocks = kv_cache_manager.allocate_slots(request, num_new_tokens)</span><br><span class="line">│   │   │    │</span><br><span class="line">│   │   │    ├─ new_blocks != <span class="built_in">None</span> → 分配成功, <span class="keyword">break</span></span><br><span class="line">│   │   │    │</span><br><span class="line">│   │   │    └─ new_blocks == <span class="built_in">None</span> → 分配失败, 需要抢占</span><br><span class="line">│   │   │       │</span><br><span class="line">│   │   │       ├─ PRIORITY策略: 抢占 running 中 priority 最高的</span><br><span class="line">│   │   │       │   └─ 若该请求已被本轮调度 → 回退 token_budget, req_index -= <span class="number">1</span></span><br><span class="line">│   │   │       │</span><br><span class="line">│   │   │       └─ FCFS策略: running.<span class="built_in">pop</span>()</span><br><span class="line">│   │   │       │</span><br><span class="line">│   │   │       ├─ _preempt_request() → num_computed_tokens=<span class="number">0</span>, 退回 waiting 队首</span><br><span class="line">│   │   │       │</span><br><span class="line">│   │   │       └─ 抢占的正好是当前请求? → <span class="keyword">break</span> (无法调度)</span><br><span class="line">│   │   │</span><br><span class="line">│   │   <span class="keyword">if</span> new_blocks is <span class="built_in">None</span> → <span class="keyword">break</span> (终止 RUNNING 遍历)</span><br><span class="line">│   │</span><br><span class="line">│   ├── ③ 记录到 scheduled_running_reqs ────────</span><br><span class="line">│   │   scheduled_running_reqs.append(request)</span><br><span class="line">│   │   num_scheduled_tokens[req_id] = num_new_tokens</span><br><span class="line">│   │   token_budget -= num_new_tokens</span><br><span class="line">│   │   req_index += <span class="number">1</span></span><br><span class="line">│   │</span><br><span class="line">│   │   (投机 token 记账 / encoder 记账)</span><br><span class="line">│</span><br><span class="line">├─ 统计 scheduled_loras</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">2</span>: WAITING 遍历 (<span class="keyword">while</span> (waiting or skipped) and token_budget &gt; <span class="number">0</span>)</span><br><span class="line">│   │</span><br><span class="line">│   ├── 容量检查 ──────────────────────────────</span><br><span class="line">│   │    num_running &gt;= max_num_running_reqs → <span class="keyword">break</span></span><br><span class="line">│   │</span><br><span class="line">│   ├── ① _select_waiting_queue_for_scheduling()</span><br><span class="line">│   │    │</span><br><span class="line">│   │    │  FCFS:  skipped_waiting 优先 (相当于阻塞优先)</span><br><span class="line">│   │    │  PRIORITY: peek 两队头, 比较 priority+arrival_time</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ▼</span><br><span class="line">│   ├── ② request_queue.peek_request()  ← 看队头，不移除</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ 阻塞状态 (REMOTE_KVS / STREAMING)?</span><br><span class="line">│   │    │   ├─ _try_promote 成功 → 继续处理</span><br><span class="line">│   │    │   └─ _try_promote 失败 → <span class="built_in">pop</span> + prepend 到 step_skipped → <span class="keyword">continue</span></span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ num_stale_output_tokens &gt; <span class="number">0</span> 且非 drop 模式?</span><br><span class="line">│   │    │   └─ 僵尸输出还在路上 → <span class="built_in">pop</span> + prepend 到 step_skipped → <span class="keyword">continue</span></span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ LoRA 上限? → <span class="built_in">pop</span> + prepend 到 step_skipped → <span class="keyword">continue</span></span><br><span class="line">│   │</span><br><span class="line">│   ├── ③ prefix cache 查找 (仅当 num_computed_tokens == <span class="number">0</span>) ────</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ get_computed_blocks() → 本地 block <span class="built_in">hash</span> 命中</span><br><span class="line">│   │    └─ KV connector → 远程 KV cache 命中 (num_external_computed_tokens)</span><br><span class="line">│   │    │</span><br><span class="line">│   │    num_computed_tokens = local + external</span><br><span class="line">│   │</span><br><span class="line">│   ├── ④ 计算 num_new_tokens ──────────────────</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ load_kv_async? → num_new_tokens = <span class="number">0</span> (只加载不做本地计算)</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ 正常计算:</span><br><span class="line">│   │       num_new_tokens = request.num_tokens - num_computed_tokens</span><br><span class="line">│   │       ├─ chunked prefill 截断</span><br><span class="line">│   │       ├─ <span class="built_in">min</span>(num_new_tokens, token_budget)</span><br><span class="line">│   │       └─ encoder 相关调整</span><br><span class="line">│   │</span><br><span class="line">│   ├── ⑤ allocate_slots() ────────────────────</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ new_blocks != <span class="built_in">None</span> → 分配成功</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ new_blocks == <span class="built_in">None</span> → 分配失败 → <span class="keyword">break</span> (终止 WAITING 遍历)</span><br><span class="line">│   │</span><br><span class="line">│   ├── ⑥ 准入 RUNNING ────────────────────────</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ load_kv_async? → status = WAITING_FOR_REMOTE_KVS → prepend skipped</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ 正常准入:</span><br><span class="line">│   │       pop_request()  ← 从 waiting 真正移除</span><br><span class="line">│   │       self.running.append(request)</span><br><span class="line">│   │       status = RUNNING</span><br><span class="line">│   │       num_computed_tokens = num_computed_tokens  (含 prefix cache 命中)</span><br><span class="line">│   │       │</span><br><span class="line">│   │       ├─ 原 status == WAITING  → scheduled_new_reqs</span><br><span class="line">│   │       └─ 原 status == PREEMPTED → scheduled_resumed_reqs</span><br><span class="line">│   │</span><br><span class="line">│   │    token_budget -= num_new_tokens</span><br><span class="line">│   │    (投机 token / encoder / LoRA 记账)</span><br><span class="line">│</span><br><span class="line">├─ 回塞: step_skipped_waiting → self.skipped_waiting (本轮跳过的重新入队)</span><br><span class="line">│</span><br><span class="line">├─ DP 记录: <span class="keyword">if</span> not defer_prefills: prefill_capacity_bound = bool(self.waiting)</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">3</span>: 组装 SchedulerOutput ────────────────</span><br><span class="line">│   new_reqs_data (v2: <span class="keyword">new</span>+resumed 合并)</span><br><span class="line">│   cached_reqs_data (running+resumed 的 block_ids, token_ids, computed_tokens)</span><br><span class="line">│   num_common_prefix_blocks</span><br><span class="line">│   kv_cache_block_copies (CoW)</span><br><span class="line">│   finished_req_ids / preempted_req_ids</span><br><span class="line">│</span><br><span class="line">├─ KV connector meta 填充</span><br><span class="line">│</span><br><span class="line">├─ sched_step_seq += <span class="number">1</span>  (仅当 total_num_scheduled_tokens &gt; <span class="number">0</span>)</span><br><span class="line">│</span><br><span class="line">└─ _update_after_schedule()  ← 更新 Request 状态</span><br><span class="line">    <span class="keyword">for</span> each scheduled request:</span><br><span class="line">      request.num_computed_tokens += num_scheduled_token</span><br><span class="line">      request.num_in_flight_tokens += num_scheduled_token</span><br><span class="line">      request.is_prefill_chunk = (computed &lt; tokens + placeholders)</span><br><span class="line"></span><br><span class="line">    ═══════════════════════════════════════</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> scheduler_output</span><br></pre></td></tr></table></figure><hr><h4 id="update-from-output-完整流程"><a href="#update-from-output-完整流程" class="headerlink" title="update_from_output() 完整流程"></a>update_from_output() 完整流程</h4><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line"><span class="built_in">update_from_output</span>(scheduler_output, model_runner_output)</span><br><span class="line">│  EngineCore<span class="selector-class">.step</span>() → <span class="built_in">schedule</span>() 之后 → <span class="built_in">execute_model</span>() 之后</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">0</span>: 初始化 + 延迟释放</span><br><span class="line">│   │</span><br><span class="line">│   ├─ defer_block_free? → processed_step_seq++ → <span class="built_in">_drain_deferred_frees</span>()</span><br><span class="line">│   │   (GPU 写入已完成, 之前延迟释放的 block 现在安全归还)</span><br><span class="line">│   │</span><br><span class="line">│   ├─ KV 加载失败处理</span><br><span class="line">│   │   if kv_connector_output.invalid_block_ids:</span><br><span class="line">│   │     <span class="built_in">_handle_invalid_blocks</span>() → 标记这些 block 需重新计算</span><br><span class="line">│   │</span><br><span class="line">│   └─ routed_experts 持久化到 scheduler 侧 slot buffer</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">1</span>: 主循环 — 遍历每个被调度的请求</span><br><span class="line">│   for req_id, num_tokens_scheduled in num_scheduled_tokens:</span><br><span class="line">│   │</span><br><span class="line">│   ├── ① 核销 in-flight ──────────────────────</span><br><span class="line">│   │    request.num_in_flight_tokens -= num_tokens_scheduled</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ num_stale_output_tokens &gt; <span class="number">0</span>? → output_is_stale = True</span><br><span class="line">│   │        num_stale_output_tokens -= num_tokens_scheduled  (逐步消化僵尸)</span><br><span class="line">│   │</span><br><span class="line">│   ├── 跳过判断 ──────────────────────────────</span><br><span class="line">│   │    ├─ KV 加载失败? → continue</span><br><span class="line">│   │    ├─ request is None or <span class="built_in">is_finished</span>()? → continue</span><br><span class="line">│   │    └─ output_is_stale and drop_stale_output? → continue  (丢弃整个<span class="string">&quot;上一世&quot;</span>输出)</span><br><span class="line">│   │</span><br><span class="line">│   ├── ② 投机解码回退 ────────────────────────</span><br><span class="line">│   │    如果 scheduled_spec_token_ids 存在:</span><br><span class="line">│   │      num_accepted = <span class="built_in">len</span>(generated_token_ids) - num_sampled</span><br><span class="line">│   │      num_rejected = num_draft_tokens - num_accepted</span><br><span class="line">│   │      │</span><br><span class="line">│   │      └─ if not output_is_stale:</span><br><span class="line">│   │           request.num_computed_tokens -= num_rejected   ← 回退</span><br><span class="line">│   │           request.num_output_placeholders -= num_rejected</span><br><span class="line">│   │</span><br><span class="line">│   ├── ③ 释放 encoder inputs ─────────────────</span><br><span class="line">│   │    (本轮已执行完毕，encoder 不再需要)</span><br><span class="line">│   │</span><br><span class="line">│   ├── ④ 判断停止 ────────────────────────────</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ 有产出 token → <span class="built_in">_update_request_with_output</span>()</span><br><span class="line">│   │    │   │</span><br><span class="line">│   │    │   ├─ 非 stale: token 写入 _all_token_ids + _output_token_ids</span><br><span class="line">│   │    │   ├─ 非 stale: num_output_placeholders -= <span class="built_in">len</span>(new_token_ids)</span><br><span class="line">│   │    │   ├─ 非 stale: <span class="built_in">cache_blocks</span>()  ← 将新 block 注册到 prefix cache</span><br><span class="line">│   │    │   └─ 检查 stop string / max_tokens / EOS → 返回 stopped</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ pooling 请求有输出 → FINISHED_STOPPED</span><br><span class="line">│   │    └─ encoder-only 且 prompt 已完 → FINISHED_STOPPED</span><br><span class="line">│   │</span><br><span class="line">│   ├── ⑤ 语法检查 (structured output) ────────</span><br><span class="line">│   │    grammar.<span class="built_in">accept_tokens</span>() → 失败则 FINISHED_ERROR</span><br><span class="line">│   │</span><br><span class="line">│   ├── ⑥ routed_experts 提取 ────────────────</span><br><span class="line">│   │    prefill 完成时: 从 slot buffer 读取完整 prompt 的专家路由</span><br><span class="line">│   │    decode 时: 只取新 token 的路由</span><br><span class="line">│   │</span><br><span class="line">│   ├── ⑦ 停止处理 ────────────────────────────</span><br><span class="line">│   │    if stopped:</span><br><span class="line">│   │      finished = <span class="built_in">_handle_stopped_request</span>()</span><br><span class="line">│   │      │</span><br><span class="line">│   │      ├─ 正常 STOP → <span class="built_in">_free_request</span>() → 释放 KV cache + encoder cache</span><br><span class="line">│   │      │   (request 设为 None, 可被 GC)</span><br><span class="line">│   │      │</span><br><span class="line">│   │      └─ 流式请求 → status = WAITING_FOR_STREAMING_REQ</span><br><span class="line">│   │          (不释放, 继续等待下一次 input)</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ 记入 stopped_running_reqs / stopped_preempted_reqs</span><br><span class="line">│   │</span><br><span class="line">│   └── ⑧ 构造 EngineCoreOutput ───────────────</span><br><span class="line">│        outputs[client_index].<span class="built_in">append</span>(</span><br><span class="line">│          new_token_ids, finish_reason, logprobs, pooling_output,</span><br><span class="line">│          prefill_stats, kv_transfer_params, events, ...</span><br><span class="line">│        )</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">2</span>: 清理 ──────────────────────────────</span><br><span class="line">│   │</span><br><span class="line">│   ├─ stopped_running_reqs → 从 self.running 移除</span><br><span class="line">│   ├─ stopped_preempted_reqs → 从 waiting + skipped 移除</span><br><span class="line">│   │</span><br><span class="line">│   ├─ grammar 编译错误 → <span class="built_in">finish_requests</span>(FINISHED_ERROR) → 追加空 output</span><br><span class="line">│   ├─ KV 加载失败且不重算 → <span class="built_in">finish_requests</span>(FINISHED_ERROR) → 追加空 output</span><br><span class="line">│   │</span><br><span class="line">│   ├─ KV connector 状态更新 (_update_from_kv_xfer_finished)</span><br><span class="line">│   └─ KV cache 事件收集 + 发布</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">3</span>: 统计 ──────────────────────────────</span><br><span class="line">│   prefix cache stats + connector stats + cudagraph stats + perf stats</span><br><span class="line">│</span><br><span class="line">└─ 构造 EngineCoreOutputs ─────────────────────</span><br><span class="line">    <span class="built_in">EngineCoreOutputs</span>(outputs, scheduler_stats, ...)</span><br><span class="line">    return &#123;client_index: EngineCoreOutputs&#125;</span><br></pre></td></tr></table></figure><hr><h4 id="两个方法的对应关系"><a href="#两个方法的对应关系" class="headerlink" title="两个方法的对应关系"></a>两个方法的对应关系</h4><figure class="highlight smali"><table><tr><td class="code"><pre><span class="line">schedule()                         update_from_output()</span><br><span class="line">  分配 KV cache               →      核销 in-flight token</span><br><span class="line">  num_computed_tokens += N    →      num_in_flight_tokens -= N</span><br><span class="line">  num_scheduled_tokens[req]=N →      读取 num_scheduled_tokens[req]</span><br><span class="line">  分配 speculative draft      →      接受/拒绝 → 回退 num_computed_tokens</span><br><span class="line">  预占 output_placeholders    →      核销 num_output_placeholders</span><br><span class="line"> <span class="built_in"> new/resumed/running/preempt </span>→      stopped → 清理 running/waiting</span><br></pre></td></tr></table></figure><h4 id="重要：调度器schedule-调度策略总结"><a href="#重要：调度器schedule-调度策略总结" class="headerlink" title="重要：调度器schedule() 调度策略总结"></a>重要：调度器schedule() 调度策略总结</h4><p>这里先做一个理解的复盘</p><ol><li>3个队列的定位</li><li>一个请求调度的数据流向</li><li>一个请求会有哪几种状态</li></ol><p>下面是剔除所有优化分支后的<strong>最简核心流程</strong>：</p><figure class="highlight lua"><table><tr><td class="code"><pre><span class="line">schedule() 被 EngineCore.step() 调用</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">0</span>: 初始化</span><br><span class="line">│   token_budget = max_num_scheduled_tokens</span><br><span class="line">│   scheduled_new_reqs / resumed / <span class="built_in">running</span> / preempted = []</span><br><span class="line">│   req_to_new_blocks = &#123;&#125;           ← req → 本轮新分配的 block</span><br><span class="line">│   num_scheduled_tokens = &#123;&#125;        ← req → 本轮调度 token 数</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">1</span>: RUNNING 遍历 ─────────────────────────────────────</span><br><span class="line">│   <span class="keyword">while</span> req_index &lt; <span class="built_in">len</span>(<span class="built_in">running</span>) <span class="keyword">and</span> token_budget &gt; <span class="number">0</span>:</span><br><span class="line">│   │</span><br><span class="line">│   │  request = <span class="built_in">running</span>[req_index]</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ① 算 num_new_tokens</span><br><span class="line">│   │    num_new_tokens = num_tokens_with_spec - num_computed_tokens</span><br><span class="line">│   │    num_new_tokens = <span class="built_in">min</span>(num_new_tokens, long_prefill_threshold, token_budget)</span><br><span class="line">│   │    num_new_tokens = <span class="built_in">min</span>(num_new_tokens, max_model_len - num_computed_tokens - <span class="number">1</span>)</span><br><span class="line">│   │</span><br><span class="line">│   ├─ num_new_tokens == <span class="number">0</span> → req_index++, continue</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ② allocate_slots() 分配 KV cache</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ 成功 → new_blocks 到手</span><br><span class="line">│   │    │</span><br><span class="line">│   │    └─ 失败 (显存不够) → 抢占 victim</span><br><span class="line">│   │       │</span><br><span class="line">│   │       ├─ FCFS: 抢 <span class="built_in">running</span> 队尾 (最年轻)</span><br><span class="line">│   │       ├─ PRIORITY: 抢 priority 最低的</span><br><span class="line">│   │       │</span><br><span class="line">│   │       ├─ victim 已在本轮名单? → 收回其 token_budget, req_index -= <span class="number">1</span></span><br><span class="line">│   │       └─ _preempt_request(victim)</span><br><span class="line">│   │           └─ num_computed_tokens = <span class="number">0</span>, 放回 waiting 队首</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ③ 记录到 scheduled_running_reqs</span><br><span class="line">│   │    scheduled_running_reqs.append(request)</span><br><span class="line">│   │    num_scheduled_tokens[req_id] = num_new_tokens</span><br><span class="line">│   │    token_budget -= num_new_tokens</span><br><span class="line">│   │    req_index += <span class="number">1</span></span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">2</span>: WAITING 遍历 ─────────────────────────────────────</span><br><span class="line">│   <span class="keyword">while</span> (waiting <span class="keyword">or</span> skipped) <span class="keyword">and</span> token_budget &gt; <span class="number">0</span>:</span><br><span class="line">│   │</span><br><span class="line">│   ├─ 容量检查: <span class="built_in">len</span>(<span class="built_in">running</span>) &gt;= max_running → <span class="keyword">break</span></span><br><span class="line">│   │</span><br><span class="line">│   ├─ ① peek 队头 request（不移除）</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ② prefix cache 查找 (仅当 num_computed_tokens == <span class="number">0</span>)</span><br><span class="line">│   │    new_computed_blocks, num_cached = get_computed_blocks(request)</span><br><span class="line">│   │    num_computed_tokens = num_cached  ← 已命中 token 数</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ③ 算 num_new_tokens</span><br><span class="line">│   │    num_new_tokens = request.num_tokens - num_computed_tokens</span><br><span class="line">│   │    num_new_tokens = <span class="built_in">min</span>(num_new_tokens, long_prefill_threshold, token_budget)</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ④ allocate_slots() 分配 KV cache</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ 成功 → new_blocks 到手</span><br><span class="line">│   │    └─ 失败 → <span class="keyword">break</span> (不抢占 waiting 请求)</span><br><span class="line">│   │</span><br><span class="line">│   ├─ ⑤ 准入 RUNNING</span><br><span class="line">│   │    pop_request()                    ← 从 waiting 真正移除</span><br><span class="line">│   │    <span class="built_in">running</span>.append(request)</span><br><span class="line">│   │    <span class="built_in">status</span> = RUNNING</span><br><span class="line">│   │    num_computed_tokens = num_computed_tokens  ← 含 prefix hit</span><br><span class="line">│   │    │</span><br><span class="line">│   │    ├─ 原 WAITING   → scheduled_new_reqs</span><br><span class="line">│   │    └─ 原 PREEMPTED → scheduled_resumed_reqs</span><br><span class="line">│   │</span><br><span class="line">│   │    num_scheduled_tokens[req_id] = num_new_tokens</span><br><span class="line">│   │    token_budget -= num_new_tokens</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">3</span>: 组装 SchedulerOutput ─────────────────────────────</span><br><span class="line">│   new_reqs_data    ← scheduled_new_reqs 的 block_ids + token_ids</span><br><span class="line">│   cached_reqs_data ← scheduled_running/resumed 的增量信息</span><br><span class="line">│   scheduler_output = SchedulerOutput(</span><br><span class="line">│       scheduled_new_reqs,</span><br><span class="line">│       scheduled_cached_reqs,</span><br><span class="line">│       num_scheduled_tokens,       ← executer 用它知道每个 req 算几步</span><br><span class="line">│       total_num_scheduled_tokens,</span><br><span class="line">│       finished_req_ids,</span><br><span class="line">│       preempted_req_ids,</span><br><span class="line">│       ...</span><br><span class="line">│   )</span><br><span class="line">│</span><br><span class="line">├─ Phase <span class="number">4</span>: _update_after_schedule() ──────────────────────────</span><br><span class="line">│   <span class="keyword">for</span> each scheduled request:</span><br><span class="line">│     request.num_computed_tokens += num_scheduled_token</span><br><span class="line">│     request.num_in_flight_tokens += num_scheduled_token</span><br><span class="line">│     request.is_prefill_chunk = computed &lt; tokens</span><br><span class="line">│</span><br><span class="line">└─ <span class="keyword">return</span> scheduler_output</span><br></pre></td></tr></table></figure><p> <strong>四个核心思想</strong></p><p><img src="/images/Pasted%20image%2020260811104109.png"></p><p>至此，我们的run_busy_loop的第一步，就分析完了 </p><p><mark style="background:#fff88f">附加的老师的关于本地、远端kvcache搜查的逻辑</mark><br><img src="/images/Pasted%20image%2020260811144712.png"></p><p><img src="/images/Pasted%20image%2020260811144920.png"></p><p><mark style="background:#fff88f">附加老师关于调度任务输出的主要内容</mark><br><img src="/images/Pasted%20image%2020260811145032.png"></p><p><img src="/images/Pasted%20image%2020260811145112.png"></p><p><img src="/images/Pasted%20image%2020260811145200.png"><br><img src="/images/Pasted%20image%2020260811145259.png"></p><p><img src="/images/Pasted%20image%2020260811145339.png"></p><p>我自己的my-vllm里面的，关于waiting队列的req的调度逻辑：</p><p><img src="/images/Pasted%20image%2020260814135441.png"><br><img src="/images/Pasted%20image%2020260814135415.png"></p><h3 id="kvcache-manager"><a href="#kvcache-manager" class="headerlink" title="kvcache manager"></a>kvcache manager</h3><p>先来看一下使用方：<br>每一个token输入，模型的每一层，都需要记录下一对k,v向量。所有层的这对kv向量显存，就是我们这个decode阶段要申请的kvcache。</p><blockquote><p>如果是prefill阶段的kvcache， 肯定就是一整块的，而不是一个token向量的。</p></blockquote><p>k,v向量的维度 &#x3D; d_h x num_head.</p><p>所以对应num_computed_tokens &#x3D; L的req来说。他目前占用的kvcache显存是：<br>L <code>*</code> d_h  <code>*</code>  num_heads</p><p><img src="/images/Pasted%20image%2020260814203331.png"></p><blockquote><p>我们worker每次执行一次前向推理，会同时处理多个req，里面的req，</p><ul><li>可能会包含new req， 需要prompt&#x2F; prompt + output的，一口气写prompt_len  <code>*</code> d_h  <code>*</code> num_heads 个 dtype的kvcache</li><li>如果是cached_req, 就只会需要写一个token的kvcache</li></ul></blockquote><p>所以kvcachemanager的设计规则如下：</p><ul><li>将可用的KVCache预先划分成一个block pool, 作为仓库<ul><li>受限因素：<ul><li>用户显存的限制，允许GPU显存使用比例</li><li>模型自身权重占掉，推理的中间张量，cuda graph, 临时buffer</li></ul></li><li>启动时执行一次profiling, 使用随机输入数据跑一遍前向传播，观察实际显存峰值占用。这样可以把权重、激活值以及运行时临时 buffer 等因素都纳入统计，得到更接近真实推理过程的显存使用情况</li><li><img src="/images/Pasted%20image%2020260814205034.png" alt="397"></li></ul></li><li>每个req 维护一个 block table 的块表<ul><li>维护一个空闲队列</li><li>维护一个hash 到 block的映射（prefix）<ul><li>为已经计算过的完整block建立hash映射</li></ul></li></ul></li><li>匹配规则<ul><li>分配显存的颗粒度是一个block</li><li>prefix cache匹配的颗粒度是block</li><li>一个req，每block_size个token计算hash，每个block的hash包含前面的hash，所以hash值本身就隐含了链式关系</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260814214859.png"></p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">新请求</span><br><span class="line"> |</span><br><span class="line"> | tokenize</span><br><span class="line"> |</span><br><span class="line">切KV block</span><br><span class="line"> |</span><br><span class="line">计算block_hashes</span><br><span class="line"> |</span><br><span class="line"> |</span><br><span class="line">查询 cached_block_hash_to_block</span><br><span class="line"> |</span><br><span class="line"> +--------------+</span><br><span class="line"> |              |</span><br><span class="line">命中           miss</span><br><span class="line"> |              |</span><br><span class="line">复用已有block    allocate新block</span><br><span class="line"> |              |</span><br><span class="line"> |              |</span><br><span class="line"> +------prefill剩余token</span><br><span class="line">                |</span><br><span class="line">                |</span><br><span class="line">          cache_blocks()</span><br><span class="line">                |</span><br><span class="line">                |</span><br><span class="line">        hash-&gt;physical block加入表</span><br></pre></td></tr></table></figure><p>所以，对于调度schedule()中，针对每个req，计算他的prefix的full block， 然后计算链式hash，然后prefix cache看看是否命中。把命中的block除掉，剩下的部分，申请显存block。然后记录在这个req的本轮新申请的blocks列表里面，发出去给执行器，交给worker执行，worker拿到这个block来计算kvcache，最后返回结果。 </p><p>那么是什么时候，把这个req没有命中但是计算出来的full block加入我们的kvcachemanager的这个prefix cache的表的呢？是update_after_schedule()还是update_from_output?</p><p>答案：</p><p><strong>不是 <code>update_after_schedule()</code>，而是在 <code>update_from_output()</code>（或者等价的模型执行完成后的更新阶段）。</strong></p><p>原因是：<strong>schedule 阶段只是决定要算什么，还没有真正算出 KV。</strong></p><p><img src="/images/Pasted%20image%2020260814220348.png" alt="304"><br><img src="/images/Pasted%20image%2020260814220335.png" alt="465"></p><p><img src="/images/Pasted%20image%2020260814220653.png" alt="481"></p><h6 id="kvcachecoordinator"><a href="#kvcachecoordinator" class="headerlink" title="kvcachecoordinator"></a>kvcachecoordinator</h6><p>我也来总结一下KVCacheManager类的主要属性</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">KVCacheManager</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">max_model_len <span class="comment"># 单个req的最大上下文kvcache长度</span></span><br><span class="line">max_in_flight_tokens = max_model_len</span><br><span class="line">num_kv_cache_groups <span class="comment"># kvcachegroup数</span></span><br><span class="line">kv_cache_config <span class="comment"># 配置</span></span><br><span class="line">watermark_blocks <span class="comment"># 最小空闲block数，避免频繁抢占</span></span><br><span class="line">empty_kv_cache_blocks <span class="comment"># 每个group一个空block，一个req如果没有分到block,就引用他</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.coordinator <span class="comment"># 构造一个单group类型的 kv协调器，取出物理block pool句柄</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 核心属性，单layer的block pool</span></span><br><span class="line"><span class="variable language_">self</span>.block_pool = <span class="variable language_">self</span>.coordinator.block_pool <span class="comment"># 取出来的block_pool句柄</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以看来物理block pool的获取，还需要依赖kvcoordinator来获取每个分组不同类型的block</p><p>单个kv_group的时候，构造的是UnitaryKVCacheCoordinator这个类</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">UnitaryKVCacheCoordinator</span>(<span class="title class_ inherited__">KVCacheCoordinator</span>):</span><br><span class="line"><span class="comment"># 基类属性</span></span><br><span class="line">kv_cache_config <span class="comment"># kvcache的属性配置</span></span><br><span class="line">max_model_len </span><br><span class="line">enable_caching <span class="comment"># flag, 开启prefixcache</span></span><br><span class="line"></span><br><span class="line">scheduler_block_size <span class="comment">#调度器下一个block 16个token</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 核心属性：单层的block id</span></span><br><span class="line"><span class="variable language_">self</span>.block_pool <span class="comment"># 构建统一内存池块，作为所有组的物理块来源</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 核心属性：多个group的管理器，来把公共的block id，解析成各自group layer的group-block id</span></span><br><span class="line"><span class="variable language_">self</span>.single_type_manager = <span class="built_in">tuple</span>(get_manager_for_kv_cache_spec <span class="keyword">for</span> i <span class="keyword">in</span> kv_cache_groups)<span class="comment"># 为每个kvcache group 构建对应的单类型管理器，形成管理链</span></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 class="comment"># 子类属性</span></span><br><span class="line">num_single_type_manager <span class="comment"># 记录管理器的个数，供后续零命中的时候使用</span></span><br></pre></td></tr></table></figure><p>所以，可以看到，这里的协调器，仅仅只是维护保存一个统一的物理block，具体针对某个kvcache block管理的，还是有下一级的管理器的。</p><h6 id="kvcachemanager-1"><a href="#kvcachemanager-1" class="headerlink" title="kvcachemanager"></a>kvcachemanager</h6><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SingleTypeKVCacheManager</span>:</span><br><span class="line">scheduler_block_size <span class="comment">#block_size</span></span><br><span class="line">block_size      <span class="comment">#一个block多少个token</span></span><br><span class="line">kv_cache_spec   <span class="comment"># 这个类型的kvcache规格</span></span><br><span class="line">kv_cache_group_id  <span class="comment">#本group类型id</span></span><br><span class="line">_null_block <span class="comment">#空block</span></span><br><span class="line"><span class="variable language_">self</span>.block_pool <span class="comment">#单卡单层的block池</span></span><br><span class="line">_max_admission_blocks_per_request <span class="comment">#每个req请求的block上限</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.new_block_ids: <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># 本batch新分配的，需要worker清零的 block_id列表</span></span><br><span class="line"><span class="variable language_">self</span>.req_to_blocks: <span class="built_in">dict</span>[req_name, <span class="built_in">list</span>[KVCacheBlock]] <span class="comment">#每个req的本group的block id，</span></span><br><span class="line"><span class="comment"># 用来调度的筹码</span></span><br><span class="line"><span class="variable language_">self</span>.num_cached_block: <span class="built_in">dict</span>[req_name, <span class="built_in">int</span>] <span class="comment">#每个req已经占用的block数</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下面我们先看一下block_pool是个什么样子的数据结构</p><p><mark style="background:#40a9ff">一个 BlockPool，N 个管理器，共享同一个池</mark></p><p><img src="/images/Pasted%20image%2020260815135336.png"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 一个 BlockPool，N 个管理器，共享同一个池</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BlockPool</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">num_gpu_blocks <span class="comment"># 单模型副本下，每张卡的最小block数量,由moder_runner测定</span></span><br><span class="line">enable_caching <span class="comment"># flag， 使能prefix cache</span></span><br><span class="line">hash_block_size <span class="comment"># 求hash的16个token</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建每张卡公共的block逻辑块的列表，用来统一管理</span></span><br><span class="line"><span class="variable language_">self</span>.blocks : <span class="built_in">list</span>[KVCacheBlock] = [KVCacheBlock(i) <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(num_gpu_blocks)]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 用我们创建出来的整个模型副本的单卡的逻辑block表，来构建一个空闲block队列</span></span><br><span class="line"><span class="variable language_">self</span>.free_block_queue : FreeKVCacheBlockQueue(<span class="variable language_">self</span>.blocks)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 构建正反向 的 块hash映射表</span></span><br><span class="line"><span class="variable language_">self</span>.cached_block_hash_to_block: BlockHashToBlockMap </span><br><span class="line"><span class="variable language_">self</span>.cached_block_hashes_by_block:<span class="built_in">dict</span>[<span class="built_in">int</span>, <span class="built_in">set</span>[BlockHashWithGroupId]]</span><br></pre></td></tr></table></figure><p>所以一个<mark style="background:#fff88f">BlockPool，块池</mark>，就是<mark style="background:#fff88f">一个模型副本</mark>下 <mark style="background:#fff88f">单类型(</mark>attention的显存) 的<mark style="background:#fff88f">单卡</mark> 的<mark style="background:#d3f8b6">空闲block队列</mark>+<mark style="background:#d3f8b6">hash块映射表</mark></p><p><img src="/images/Pasted%20image%2020260815151445.png"></p><p><code>_block_hash</code> &#x3D; 「链式内容哈希 + 所属 group 编号」拼成的一个 bytes 键。哈希管「内容是什么」，group_id 管「这块存在哪一类 KV block pool 里」，两者拼一起才是前缀缓存里真正拿来查表的完整 key。</p><p>我们来看一下一个逻辑block块是如何实现的，里面究竟有什么：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">KVCacheBlock</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">block_id <span class="comment"># 单卡内的逻辑block编号：0 - num_gpu_blocks-1</span></span><br><span class="line">ref_cnt <span class="comment"># 被引用计数</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【内容hash链 - block_id 信息】</span></span><br><span class="line">_block_hash: BlockHashWithGroupId <span class="comment"># full block的链式hash值+groupid(还没分组呢)</span></span><br><span class="line">_block_hash_num_tokens <span class="comment">#这个 block 的哈希覆盖了多少个前缀 token</span></span><br><span class="line"></span><br><span class="line">prev_free_block <span class="comment"># 前一个block指针</span></span><br><span class="line">next_free_block <span class="comment"># 后一个block指针</span></span><br><span class="line"></span><br><span class="line">is_null <span class="comment"># flag， 标记这个block永远不擦怒缓存，占位用</span></span><br></pre></td></tr></table></figure><p>我们来看一下这个空闲队列是如何实现的：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">FreeKVCacheBlockQueue</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">num_free_blocks <span class="comment"># 逻辑单卡上的可用block数</span></span><br><span class="line"></span><br><span class="line">(把KVCacheBlocks收尾相接，做成双向空闲block链表)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 头尾dummy指针，然后接上去</span></span><br><span class="line">fake_free_list_head</span><br><span class="line">fake_free_list_tail</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以对于一个KVCacheCoordinator这样一个协调基类，他创建一个BlockPool, 这个里面根据num_gpu_blocks 来创建出空的KVCacheBlock，然后把他们串成双向链表，这个时候，这个就是一个单卡的逻辑显存块池，此时里面的block块还没有分组，因为都是空闲的。</p><p>接下来我们看一下KVCacheCoordinator， 他为每一个group的显存组，都创建一个对应的管理器是如何实现的</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">       <span class="variable language_">self</span>.single_type_managers = <span class="built_in">tuple</span>(</span><br><span class="line">            get_manager_for_kv_cache_spec(</span><br><span class="line">                kv_cache_spec=kv_cache_group.kv_cache_spec,</span><br><span class="line">                max_in_flight_tokens=max_in_flight_tokens,</span><br><span class="line">                max_model_len=max_model_len,</span><br><span class="line">                block_pool=<span class="variable language_">self</span>.block_pool,</span><br><span class="line">                enable_caching=enable_caching,</span><br><span class="line">                kv_cache_group_id=i,</span><br><span class="line">                dcp_world_size=dcp_world_size,</span><br><span class="line">                pcp_world_size=pcp_world_size,</span><br><span class="line">                scheduler_block_size=<span class="variable language_">self</span>.scheduler_block_size,</span><br><span class="line">                needs_kv_cache_zeroing=<span class="variable language_">self</span>.kv_cache_config.needs_kv_cache_zeroing,</span><br><span class="line">            )</span><br><span class="line">            <span class="keyword">for</span> i, kv_cache_group <span class="keyword">in</span> <span class="built_in">enumerate</span>(<span class="variable language_">self</span>.kv_cache_config.kv_cache_groups)</span><br><span class="line">        )</span><br><span class="line"><span class="comment"># 这里经常看到spec, 这个spec是规格的意思，就是配置</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 可以看到他这里面枚举了self.kv_cache_config.kv_cache_groups</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">KVCacheConfig</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">num_blocks <span class="comment">#显存profile后的逻辑单卡的 总块数</span></span><br><span class="line">kv_cache_tensors: <span class="built_in">list</span>[KVCacheTensor]</span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">class KVCacheTensor:</span></span><br><span class="line"><span class="string">size                 # kvcache张量的字节数</span></span><br><span class="line"><span class="string">shared_by: list[str] # 共享相同 kvcache张量 的 层名</span></span><br><span class="line"><span class="string">offset               # 这一层的</span></span><br><span class="line"><span class="string">block_stride</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line">kv_cache_groups: <span class="built_in">list</span>[KVCacheGroupSpec]</span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">class KVCacheGroupSpec:</span></span><br><span class="line"><span class="string">layer_names: list[str]</span></span><br><span class="line"><span class="string">kv_cache_spec: KVCacheSpec</span></span><br><span class="line"><span class="string">is_eagle_group </span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line">        </span><br><span class="line"></span><br></pre></td></tr></table></figure><p><mark style="background:#fff88f">关于KVCacheTensor类的理解，为什么要设计这个类，原因是，一个KVCacheTensor类，表示一种形状的kvcache的显存空间及其内部划分。</mark><br><img src="/images/Pasted%20image%2020260815161816.png"></p><p><img src="/images/Pasted%20image%2020260815162925.png"></p><ul><li>所以前面kvcachetensor，是描述同一种形状的layer，他们的kvcache显存的整体的tensor的一个类。是用<mark style="background:#fff88f">来描述他们的显存布局的</mark> </li><li>后面的kvcachegroupspec, 则是用来描述同一种形状的kvcache的layer，他们这一组，每个层的层名，以及任意层的kvcache的规格。<mark style="background:#fff88f">是用来描述形状的</mark>。</li></ul><p>所以我们的KVCacheConfig配置类</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">KVCacheConfig</span>:</span><br><span class="line">num_blocks <span class="comment"># profiling 测出来的</span></span><br><span class="line">kv_cache_tensors: <span class="built_in">list</span>[KVCacheTensor] <span class="comment">#各种类型的kvcache的显存区域的列表</span></span><br><span class="line">kv_cache_groups: <span class="built_in">list</span>[KVCacheGroupSpec] <span class="comment">#各种类型的kvcache的组的列表，每个组有xx层，以及kvcache的形状</span></span><br></pre></td></tr></table></figure><p><mark style="background:#fff88f">所以这个时候，你在看这个，就知道他是针对每种类型的kvcache组，来分别创建不同kvcache类型的管理器</mark><br><img src="/images/Pasted%20image%2020260815163238.png"></p><blockquote><p>这里加深了kvcache group的理解</p></blockquote><p>下面来看一下KVCacheSpec那个类，就是描述每个group的kvcache的形状的类，里面只有描述block_size的属性。</p><p><img src="/images/Pasted%20image%2020260815163724.png" alt="536"><br>其实，这是一个基类，<br><img src="/images/Pasted%20image%2020260815163802.png"><br>这里的子类真正列出了一个attention的kvcache的形状：</p><ul><li>头数</li><li>头维</li><li>数据类型</li><li>量化模式</li><li>等等</li></ul><p>除此之外还有Mamba的kvcache的类型<br><img src="/images/Pasted%20image%2020260815163915.png"></p><p>所以这个时候，就很容易理解，KVCacheCoordinator的构造中</p><ul><li>self.block_pool构造所有组的物理块的抽象block列表<ul><li><strong>block 不是「形状还没定」，而是「永远没有形状」</strong>——形状从来就不属于 block。</li></ul></li><li>为每个group形状的kvcache，分别创建对应的管理器，来把真实的block形状和block_pool的抽象的block挂上钩<br><img src="/images/Pasted%20image%2020260815164522.png"></li></ul><p>trace代码后，我们开始构造<mark style="background:#fff88f">全注意力的管理器</mark><br><img src="/images/Pasted%20image%2020260815165232.png"></p><p><img src="/images/Pasted%20image%2020260815165324.png"></p><p>来看看基类，这是一个单类型的管理器</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SingleTypeKVCacheManager</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">scheduler_block_size</span><br><span class="line">block_size</span><br><span class="line">kv_cache_spec <span class="comment"># 这里已经是attentionspec了</span></span><br><span class="line">block_pool <span class="comment"># 构造的时候，传入的KVCacheCoordinator的 抽象block池</span></span><br><span class="line">enable_caching <span class="comment">#flag</span></span><br><span class="line"><span class="variable language_">self</span>.new_block_ids : <span class="built_in">list</span>[<span class="built_in">int</span>]</span><br><span class="line"><span class="variable language_">self</span>.req_to_blocks : <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">list</span>[KVCacheBlock]] <span class="comment"># 每个req这种group类型的kvcache block列表</span></span><br><span class="line"><span class="variable language_">self</span>.num_cached_block : <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">int</span>] <span class="comment"># 每个req在这种group类型，已经占了多少个kvcache block数量</span></span><br><span class="line"><span class="variable language_">self</span>.kv_cache_group_id <span class="comment"># 这个group类型ID</span></span><br></pre></td></tr></table></figure><p>所以，是不同group的管理器内部，分别维护一个req在这个模型副本上的不同group的kvcache block列表，KVCacheCoordinator里面的block_pool只是表示抽象的block资源。</p><blockquote><p>那为什么我们从来没有看到KVCacheBlock这个抽象block块和每个卡里面的实际block物理ID挂上钩呢?</p></blockquote><p>这里的block_id， 就是在KVCacheCoordinator里面的self.block_pool里面的每个KVCacheBlock的block_id，应该是逻辑编号，是这里的range num_gpu_blocks里面的idx吧， 他设置好了这个之后，后面构造具体group的管理器的时候，就获得了我们的每个block的真实显存大小，所以之后，就直接让worker在他申请好的这一大块显存里面，取找就行了是吗？因为workerprofile之后，就直接申请好了每个group的显存区域，所以才有我们的num_gpu_blocks的是吗？</p><p><strong>「逻辑编号」和「物理下标」在这里是同一个数，不存在第二层翻译</strong></p><p>对，<a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/block_pool.py#L175">block_pool.py:175-177</a>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="variable language_">self</span>.blocks = [KVCacheBlock(idx) <span class="keyword">for</span> idx <span class="keyword">in</span> <span class="built_in">range</span>(num_gpu_blocks)]</span><br></pre></td></tr></table></figure><p><code>block_id</code> 就是 <code>0, 1, 2, ..., num_gpu_blocks-1</code>。你叫它「逻辑编号」没错，但关键是：</p><blockquote><p><strong>这个逻辑编号，直接拿来当 GPU 张量第一维的下标用，不用再翻译。</strong></p></blockquote><p>也就是说，不存在「逻辑 block 5 → 物理 block 17」这种映射。逻辑 5 就是物理 5。我之前说「block_id 本身就是物理 block id」，意思是「CPU 账本里那个整数，原封不动就是 GPU 上的 slot 下标」，不是说有两个编号体系。</p><hr><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">① worker profiling → available_memory（字节）</span><br><span class="line">② 统一 page_size → 公共 page_size_bytes </span><br><span class="line">③ num_gpu_blocks <span class="operator">=</span> available_memory // page_size_bytes   ← 关键数字在这诞生</span><br><span class="line">④ 这个 num_gpu_blocks 回传给 engine，两边各用一份：</span><br><span class="line">   ├─ CPU：BlockPool 造 num_gpu_blocks 个 KVCacheBlock（账本）</span><br><span class="line">   └─ GPU：worker 分配张量，第一维 <span class="operator">=</span> num_gpu_blocks（真实显存）</span><br></pre></td></tr></table></figure><p><strong><code>num_gpu_blocks</code> 是在「账本」和「显存」都还不存在之前，先由 profiling 算出来的一个数</strong>，然后两边各自拿同一个数去建自己的东西。所以你说的「才有我们的 num_gpu_blocks」——对，它来自 profiling。</p><p>你的理解对：block_id 就是 <code>range(num_gpu_blocks)</code> 的 idx，<code>num_gpu_blocks</code> 由 profiling 定；worker 拿到这个数后<strong>一次申请一整块连续显存</strong>，reshape 成第一维 &#x3D; num_gpu_blocks 的张量；之后用 block_id 直接当第一维下标去「找」那一块的 KV。<strong>「逻辑编号」和「物理下标」是同一个整数，中间没有翻译表。</strong></p><p>所以接下来的重心在于worker init kvcache, 看看是如何计算单group类型的num_gpu_blocks的</p><h4 id="自己关于block-pool-和-初始化kvcache显存的理解"><a href="#自己关于block-pool-和-初始化kvcache显存的理解" class="headerlink" title="自己关于block_pool 和 初始化kvcache显存的理解"></a>自己关于block_pool 和 初始化kvcache显存的理解</h4><p>下面我来说一下我的一个关于model_runner kvcache显存初始化的理解。</p><p>num_gpu_blocks 表示可用的block数，但是我理解是，<mark style="background:#fff88f">这个是一个模型副本，所有层，共用的一个层内block数</mark>。</p><p>因为我发现假设，我们现在的模型，有两个group类型的kvcache张量，假设模型一共20层，0-9层是group0, 每个token的k,v向量是1024维度，都假设一个block_size&#x3D;16个token, 所以group0区域的一个block大小假设是16MB，</p><p>后10-19层是group1, 每个token的k,v向量是2048维度，假设还是block_size&#x3D;16, 所以group1区域的一个block大小是32MB</p><p>重点来了，<mark style="background:#fff88f">因为我们模型副本的层数，形状，不同group的层数比例是固定的</mark>，也就是说，输入一个token，每个层都会产生一个token的kv向量，输入16个token，每个层都会产生一个对应group类型的block，所以这个显存消耗是固定的。比如一个token输入，会占用128MB的kvcache, 其中就固定会有比如72MB的group0, 56MB的group1。</p><p>所以下面我来说一下我对model_runner初始化kvcache显存的一个猜测</p><p>所以model_runner的dummy_run，就是利用这个比例固定，假设最后测定下来，可以给kvcache使用的显存是48G，所以，根据我们上面的72MB:56MB的比例，就可以直接划分27G给group0的tensor, 划分21G给group1的tensor。然后在各自的tensor里面，划分每一层，比如group0的Tensor，里面划分了10个layer,分别属于layer0-9, group1的tensor里面划分了10个layer,分别属于10-19.</p><p>然后每个layer区域的显存块里面，根据group的block形状，又切分出layer内的block。</p><p>所以我们kvcachemanager，kvcachecoordinator，用到的num_gpu_blocks，其实是指的任意group内的任意layer区域里面的block的数量，</p><p>因为我们一个layer用多少block，其实就已经决定了同group内任意layer用多少block，不同group的不同layer用多少block。</p><p>所以，num_gpu_blocks表示的是整个模型副本里面，单层的可用显存block数。</p><p>所以当我们调度器让kvcachemanager allocate slots 分配一个block， 从我们的空闲队列里面拿出一个block，底层是让各自group的管理器去真正的去各自的KVCacheTensor里面找到对应的各层的block的逻辑偏移位置是吗？</p><p>我理解的对面？请你简单，简洁，直接的回答我，不要长篇大论，不要模棱两可</p><hr><p><mark style="background:#fff88f">实际上，这边我理解的对了一半，主要在分配上错了，num_gpu_blocks的理解对了。</mark></p><p><img src="/images/Pasted%20image%2020260816101418.png"><br><img src="/images/Pasted%20image%2020260816101436.png"></p><p><img src="/images/Pasted%20image%2020260816101358.png"></p><ul><li>按KVCacheTensor为每个layer分配固定字节的raw buffer<ul><li>allocate_kv_cache_tensors</li></ul></li><li>每个tensor reshape出block维度</li><li>block_id in range(num_gpu_blocks), 是每个tensor里面的物理偏移。</li></ul><h3 id="profiling-kv-cache"><a href="#profiling-kv-cache" class="headerlink" title="profiling kv cache"></a>profiling kv cache</h3><p>调度器构造kvcachemanager的核心就是已经知道num_gpu_blocks， </p><p>这个是worker在profiling后，根据峰值显存占用 + 我们的kv_cache_spec 来申请划分好kvcache的显存块，然后把物理显存块的顺序id，当成单卡的逻辑id，这样我们enginecore拿到num_gpu_blocks就可以直接从0 - (num_gpu_blocks-1) 的逻辑id，直接等同于每个卡的物理blockid。</p><p>下面来分析是如何profiling的，先看一下一个卡里面的显存的三种使用情况</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># vllm启动前</span></span><br><span class="line"><span class="number">1.</span> 1G（其他进程）</span><br><span class="line"><span class="number">2.</span> 0G（模型+层间输入输出缓冲区）</span><br><span class="line"><span class="number">3.</span> 0G（通信NCCL占用，算子后端）</span><br><span class="line">   </span><br><span class="line"><span class="comment"># vllm启动后，加载完模型，暂停</span></span><br><span class="line"><span class="number">1.</span> 1G （其他进程）</span><br><span class="line"><span class="number">2.</span> 2G （模型权重）</span><br><span class="line"><span class="number">3.</span> <span class="number">0.5</span>G （通信库NCCL）</span><br><span class="line"></span><br><span class="line"><span class="comment"># profiling (peak) 所谓峰值，就是根据每次调度的最大token数，batch的最大req数，塞满。就比如说，构造一个超长的prefill，然后进行一次计算，这样可以把输入输出缓冲区拉到最大，attention算子也需要额外显存</span></span><br><span class="line"><span class="number">1.</span> 1G（其他进程）</span><br><span class="line"><span class="number">2.</span> 4G（模型2G，输入输出缓冲区2G）</span><br><span class="line"><span class="number">3.</span> 1G（NCCL0<span class="number">.5</span>G， attention backends里面比如中间量也需要缓冲区<span class="number">0.5</span>G）</span><br><span class="line">   </span><br><span class="line"><span class="comment"># profile结束</span></span><br><span class="line"><span class="number">1.</span> 1G</span><br><span class="line"><span class="number">2.</span> 3G（模型2G + 被gc回收过的剩余激活输入输出缓冲张量1G）</span><br><span class="line"><span class="number">3.</span> 1G（一样）</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里要先提一下下面几个点：</p><ul><li><p><strong>输入输出缓冲区的管理</strong>：</p><ul><li>在kuipa项目里面，我们测试都是输入一个req，prefill阶段需要Nxdim的输入输出缓冲区，decode阶段就需要1个张量输入输出缓冲区。所以我们的选择是直接提前申请输入输出缓冲区。</li><li>但是在vllm里面，每次到一张显卡的batch是不定长的。所以在不同情况有不同方法：<ul><li>正常推理过程：<ul><li>主要靠 PyTorch CUDA caching allocator（pytorch的tensor变量的内存会有专门的显存池）</li><li><img src="/images/Pasted%20image%2020260815211417.png" alt="148"></li></ul></li><li>profile peak期间：<ul><li>他就是通过构造dummy 撑满一个batch, 然后profile推理，触发pytorch cuda allocator pool的机制来自动创建中间的激活tensor, 看看他最高能触发多少，这个就是我们的峰值显存。</li></ul></li></ul></li></ul></li><li><p>attention backends的中间变量的显存占用</p><ul><li><img src="/images/Pasted%20image%2020260815211914.png" alt="215"></li></ul></li></ul><h4 id="SchedulerOutput"><a href="#SchedulerOutput" class="headerlink" title="SchedulerOutput"></a>SchedulerOutput</h4><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SchedulerOutput</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">scheduled_new_reqs : <span class="built_in">list</span>[NewRequestsData] <span class="comment"># 本轮调度的新req名单(pro)</span></span><br><span class="line">scheduled_cached_reqs : CachedRequestData <span class="comment"># 本轮调度的旧req名单</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【token id (投机解码草稿token)】</span></span><br><span class="line">scheduled_spec_decode_tokens: <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">list</span>[<span class="built_in">int</span>]] <span class="comment"># 每个req的投机解码的草稿token列表</span></span><br><span class="line">num_invalid_spec_tokens: <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">int</span>] <span class="comment"># 每个req的被拒绝的草稿token列表</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【token数统计信息】</span></span><br><span class="line">num_scheduled_tokens : <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">int</span>] <span class="comment"># 本轮req-各自token数的列表</span></span><br><span class="line"><span class="comment">## 杂项统计</span></span><br><span class="line">num_common_prefix_blocks: <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># 本轮batch的公共前缀，用于cascade attention</span></span><br><span class="line">total_num_scheduled_tokens : <span class="built_in">int</span> <span class="comment"># 本轮总共要计算的tokens数</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># --------------------------------------------------------------------------</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">finished_req_ids: <span class="built_in">set</span>[<span class="built_in">str</span>] <span class="comment"># 上一轮已经完成的req</span></span><br><span class="line">preempted_req_ids: <span class="built_in">set</span>[<span class="built_in">str</span>] <span class="comment"># 本轮被抢占，赶回waiting队列的req的集合</span></span><br><span class="line">new_block_ids_to_zero: <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># 本轮新分配，需要清零的block</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 每个新请求类</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">NewRequestsData</span>:</span><br><span class="line">req_id <span class="comment"># 请求ID</span></span><br><span class="line">sampling_params : SamplingParams <span class="comment"># 这个req的采样参数</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【token ids 信息】</span></span><br><span class="line">prompt_token_ids : <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># prompt的tokenids</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【req的token数统计信息】</span></span><br><span class="line">num_computed_tokens : <span class="built_in">int</span> <span class="comment"># 已经计算过的token数</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【req 的 单独block id信息】</span></span><br><span class="line">block_ids : <span class="built_in">tuple</span>[<span class="built_in">list</span>[<span class="built_in">int</span>],...] <span class="comment"># 该req占用的kv cache block id</span></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 class="comment"># 继续req们的增量信息</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CacheRequestData</span>:</span><br><span class="line">req_ids : <span class="built_in">list</span>[<span class="built_in">str</span>] <span class="comment"># 本轮被增量调度的req的id集合</span></span><br><span class="line">resumed_req_ids: <span class="built_in">set</span>[<span class="built_in">str</span>] <span class="comment"># 本轮被恢复过来运行的请求</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【token 数统计信息】</span></span><br><span class="line">num_computed_tokens: <span class="built_in">list</span>[<span class="built_in">int</span>]</span><br><span class="line">num_output_tokens: <span class="built_in">list</span>[<span class="built_in">int</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 【req的 单block_id信息】</span></span><br><span class="line">new_block_ids: <span class="built_in">list</span>[<span class="built_in">tuple</span>[<span class="built_in">list</span>[<span class="built_in">int</span>], ...]</span><br></pre></td></tr></table></figure><h4 id="Request"><a href="#Request" class="headerlink" title="Request"></a>Request</h4><p>我们再来看一下一个请求类的内部有哪些属性，帮助我们理解</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Request</span>&#123;</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">request_id      <span class="comment"># 请求ID</span></span><br><span class="line">client_index    <span class="comment"># 引擎前端的索引</span></span><br><span class="line">priority  <span class="comment"># req的优先级</span></span><br><span class="line">sampling_params <span class="comment"># 采样参数</span></span><br><span class="line">arrival_time  <span class="comment"># 请求的到达时间</span></span><br><span class="line">status    <span class="comment"># 请求的状态 = RequestStatus.WAITTING</span></span><br><span class="line">events : <span class="built_in">list</span>[EngineCoreEvent] <span class="comment"># 该请求的引擎事件列表</span></span><br><span class="line">session_id <span class="comment"># 会话ID，关联同一会话的多个req</span></span><br><span class="line">max_tokens = sampling_params.max_tokens <span class="comment"># 最大输出token数</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#【token ids相关】</span></span><br><span class="line">prompt_token_ids <span class="comment"># prompt的token id列表</span></span><br><span class="line">spec_token_ids <span class="comment"># 投机解码的草稿token id 列表</span></span><br><span class="line">_output_token_ids <span class="comment"># decode出来的token id列表</span></span><br><span class="line">output_token_ids <span class="comment"># 只读的输出token id列表</span></span><br><span class="line">_all_token_ids <span class="comment"># prompt + decode 的全量tokenid列表</span></span><br><span class="line">all_token_ids <span class="comment"># 只读的prompt + decode 的token id列表</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#【token 数统计信息】</span></span><br><span class="line">num_prompt_tokens <span class="comment"># prompt的token数</span></span><br><span class="line">num_in_flight_tokens <span class="comment"># 在途中处理的token数，在异步调度</span></span><br><span class="line">num_computed_tokens <span class="comment"># 已经计算出kvcache 的token 数</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【token_id - hash 信息】</span></span><br><span class="line"><span class="comment"># 用来更新链式hash，以及维护一个req的链式块hash表</span></span><br><span class="line">block_hashes: <span class="built_in">list</span>[BlockHash] </span><br><span class="line"><span class="comment"># 链式内容hash，req负责维护内容-链式hash， </span></span><br><span class="line"><span class="comment">#kvcachemanager负责维护  链式hash-&gt;block</span></span><br><span class="line">_block_hasher <span class="comment"># 一个块hash的获取器吧</span></span><br><span class="line">update_block_hashes() <span class="comment"># 计算一个块的链式hash</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># ---------------------------------------------------------</span></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 class="comment"># 调度状态相关，局部属性，不是跨模态用的</span></span><br><span class="line">is_prefill_chunk = <span class="literal">False</span> <span class="comment"># True表示该req处于非最后一块chunked的prefil阶段被调度</span></span><br><span class="line">num_preemptions = <span class="number">0</span> <span class="comment"># 被调度器抢占的次数</span></span><br><span class="line">skip_reading_prefix_cache <span class="comment"># flag, 是否跳过读取prefix cache</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以，一个req发过来，kvcachemanager, worker接受这些req，天然就知道这个req，他prompt, output的tokenids, 他目前已经计算了的token到哪里</p><p>但是注意，每个req内部，<mark style="background:#fff88f">并不知道这个req占用了哪些物理blockID</mark> , block_hashes里面存放的是内容hash, 它回答的问题是：<strong>「这 16 个 token 的内容是什么？」</strong>，而不是**「它们放在 GPU 显存的第几个 block 里？」**。</p><p>同一个内容（比如 100 个请求都是 <code>&quot;The quick brown fox...&quot;</code> 开头），它们的 <code>block_hashes</code> 全一样，但它们可能占用<strong>不同的物理 block_id</strong>（或者通过前缀缓存复用同一个 block_id）</p><p>那「占用了哪些 block」记在哪？</p><p>记在 <strong><code>KVCacheManager</code> 的 coordinator 里</strong>，不是 <code>request</code> 上。你要查某个 req 现在占哪些 block_id，是这么调：</p><p><a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/kv_cache_manager.py#L815">kv_cache_manager.py:815</a>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">get_block_ids</span>(<span class="params">self, request_id: <span class="built_in">str</span></span>) -&gt; <span class="built_in">tuple</span>[<span class="built_in">list</span>[<span class="built_in">int</span>], ...]:</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">self</span>.get_blocks(request_id).get_block_ids()</span><br></pre></td></tr></table></figure><p>这就是调度器里 <code>self.kv_cache_manager.get_block_ids(rid)</code> 那套调用（<a href="vscode-webview://0v6anhu7rvonmrg5eum0t53r31phukqu6outl7a0k1sdqa1epd0b/vllm/v1/core/sched/scheduler.py#L3729">scheduler.py:3729</a> 等），返回的是<strong>真实的 <code>block_id</code> 列表</strong>。</p><blockquote><p><mark style="background:#fff88f">req里面的这个就是存放的内容hash，里面包含了链式逻辑，然后kvcache里面存放的是hash-&gt;block的映射逻辑，所以中间加了一层hash, 所以两个结合，就可以支持prefix cache的快速索引了</mark></p></blockquote><h3 id="3-2-执行器exculator"><a href="#3-2-执行器exculator" class="headerlink" title="3.2_执行器exculator"></a>3.2_执行器exculator</h3><h4 id="executor-workers架构"><a href="#executor-workers架构" class="headerlink" title="executor - workers架构"></a>executor - workers架构</h4><p><img src="/images/Pasted%20image%2020260811151302.png"></p><p>简单理解就是workers就是一张卡上的干活的进程，就是我们kuipa的demo层的逻辑</p><p>单 GPU 场景下 Worker 就是<strong>一个完整的模型推理单元</strong>。</p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">EngineCore</span><br><span class="line">  ├── <span class="params">scheduler:</span> SchedulerInterface     ← <span class="params">CPU:</span> 选请求、分 token</span><br><span class="line">  ├── <span class="params">model_executor:</span> Executor          ← 抽象层</span><br><span class="line">  │       └── Worker (GPU)              ← 持有完整模型，跑完整前向</span><br><span class="line">  │             ├── <span class="params">model:</span> LlamaForCausalLM   (<span class="number">32</span> 层 Transformer)</span><br><span class="line">  │             ├── KV cache tensors          (预分配的 K<span class="symbol">/V</span> 显存)</span><br><span class="line">  │             └── execute_model()</span><br><span class="line">  │                   ├── prepare_input()     ← SchedulerOutput → PyTorch tensor</span><br><span class="line">  │                   ├── model.forward()     ← 完整 <span class="number">32</span> 层前向</span><br><span class="line">  │                   └── sample()            ← logits → 采样下一个 token</span><br><span class="line">  └── ZMQ <span class="symbol">/</span> IO 线程</span><br></pre></td></tr></table></figure><p>多 GPU 时每个 GPU 上各一个 Worker，通过 TP（层内切分）或 PP（层间切分）协作。但对你来说，<code>Worker = 持有模型 → 接收 SchedulerOutput → 跑前向 → 返回结果</code>，就是这四件事。</p><p><img src="/images/Pasted%20image%2020260809233611.png"></p><p>Worker 绑定一个 GPU 设备，持有该 GPU 上的模型分片和 KV cache 显存。Scheduler 只有一个跑在 CPU 上，给所有 Worker 安排工作。</p><p><img src="/images/Pasted%20image%2020260809233805.png"></p><p>Worker 每步收到一个完整的 batch——里面同时有 prefill 和 decode——跑一次 <code>model.forward()</code> 把所有请求一起算完。没有 “盯着请求 A 直到跑完” 这回事，每步的 batch 都可能不一样。这就是 continuous batching：Worker 每步处理的是一个<strong>请求 mix</strong>，而不是某个请求的完整生命周期。</p><hr><p>执行器executor的类型：</p><p><img src="/images/Pasted%20image%2020260811151628.png"></p><p><img src="/images/Pasted%20image%2020260812145049.png"></p><blockquote><p>所以我的理解没有错误，MultiprocExecutor就是单机多卡的执行器</p></blockquote><h4 id="MessageQueue"><a href="#MessageQueue" class="headerlink" title="MessageQueue"></a>MessageQueue</h4><p>这是一个进程间通信的封装层，内部封装了两种进程间通信方式：</p><ul><li>共享内存</li><li>ZMQ.PUB,SUB</li></ul><p>这样对于executor, worker之间的通信来说，就直接把batch请求任务，往这个队列里面丢，就无需关系这个通信方式，包的大小。这个通信层内部，自己会根据这个batch包的大小，选择合适的底层方案。</p><p>还有一个设计点，这个MessageQueue的设计，上面说的是针对Message, 这里的Queue, 实际上是说，<br>MessageQueue其实是一个环形缓冲区，</p><p>所以这个<strong>MessageQueue</strong>是一个封装了共享内存+zmq.PUB,SUB 的 <strong>有多个batch槽位的环形缓冲区</strong>，专门为了应对<strong>异步调度的多轮batch</strong>。如果是同步呢？也能用，不过就直接发一个batch, 消费一个batch了</p><p>同步调度时 ring buffer 大部分槽位空着而已，队列入队一个、出队一个、再入队下一个，退化成单槽行为。但代码不用改——抽象层让你不需要关心上层调度是同步还是异步，只管往里丢、从里面取</p><h4 id="worker进程的启动"><a href="#worker进程的启动" class="headerlink" title="worker进程的启动"></a>worker进程的启动</h4><ul><li>WorkerProc.make_worker_process， 工厂函数<ul><li>WorkerProc.worker_main 工厂函数<ul><li>WorkerProc.<strong>init</strong></li><li>WorkerProc.worker_busy_loop()<ul><li>接收到RPC的func，执行任务调用</li></ul></li></ul></li></ul></li></ul><figure class="highlight crystal"><table><tr><td class="code"><pre><span class="line">WorkerProc.__init__()                          <span class="comment"># 进程入口</span></span><br><span class="line">  │</span><br><span class="line">  ├─ wrapper = WorkerWrapperBase(...)          <span class="comment"># ① 创建适配层</span></span><br><span class="line">  │</span><br><span class="line">  ├─ wrapper.init_worker(all_kwargs)           <span class="comment"># ② 适配层内部:</span></span><br><span class="line">  │   └─ <span class="keyword">self</span>.worker = Worker(...)             <span class="comment">#    创建 GPU Worker (一开始就是 Worker)</span></span><br><span class="line">  │       ↑ 存为 WorkerWrapperBase.worker</span><br><span class="line">  │</span><br><span class="line">  ├─ <span class="keyword">self</span>.worker = wrapper                    <span class="comment"># WorkerProc.worker → WorkerWrapperBase</span></span><br><span class="line">  │</span><br><span class="line">  ├─ <span class="keyword">self</span>.worker.init_device()                <span class="comment"># ③ WorkerWrapperBase.init_device()</span></span><br><span class="line">  │   └─ <span class="keyword">self</span>.worker.init_device()            <span class="comment">#    → Worker.init_device()</span></span><br><span class="line">  │       └─ <span class="keyword">self</span>.model_runner = GPUModelRunner(...)  <span class="comment"># 创建 ModelRunner</span></span><br><span class="line">  │</span><br><span class="line">  └─ <span class="keyword">self</span>.worker.load_model()                 <span class="comment"># ④ 加载模型权重</span></span><br><span class="line">      └─ <span class="keyword">self</span>.worker.load_model()             <span class="comment">#    → Worker.load_model()</span></span><br></pre></td></tr></table></figure><p>所以三个对象的创建顺序是 <strong><code>Worker</code> → <code>WorkerWrapperBase</code> → <code>WorkerProc</code></strong>，而 <code>GPUModelRunner</code> 是 <code>init_device</code> 时才创建的。<code>Worker</code> 类型从一开始就确定了，不是 <code>init_device</code> 时切换的。</p><blockquote><p>所以，WorkerWrapperBase是充当WorkerProc和Worker的中间层的，WorkerProc收到的RPC任务，WorkerWrapperBase这个装饰器实例负责来解析这个RPC，然后调用Worker的方法</p></blockquote><p>就是这个关系。但它的角色不是”装饰器”，更准确的叫法是 <strong>适配层 + 拦截器</strong>。</p><p>它拦截所有 RPC 调用，做两件事：</p><ol><li><strong>多模态注入</strong>（有 work 时）— 在 <code>execute_model</code> 之前，把预处理好的图片特征注入 <code>scheduler_output</code></li><li><strong>透传</strong>（没 work 时）— 直接把调用转发给 Worker</li></ol><p>多模态部分我们先不关心，在透传的时候，就是转发RPC给Worker实例。</p><h4 id="总结：从EngineCoreProc类的构造-Worker类的构造时间线梳理"><a href="#总结：从EngineCoreProc类的构造-Worker类的构造时间线梳理" class="headerlink" title="总结：从EngineCoreProc类的构造 - Worker类的构造时间线梳理"></a>总结：从EngineCoreProc类的构造 - Worker类的构造时间线梳理</h4><p>这边我先简单总结一下vllm里面类设计的习惯</p><ul><li>新的进程，启动，一般使用xxxProc类的工厂函数<ul><li>构造xxxProc类实例，进入xxxProc.init(), 这个类一般用来作为进程间通信层，来实现握手连接等，或者增加一些线程来监听输入输出等。<ul><li>xxxProc.init()中，才开始构建真正的xxx实例<ul><li>xxx.init()，<ul><li>xxx可能是一个xxxWrapper装饰器<ul><li>他的内部包含子类，这个装饰器的作用是一个适配器，用来选择合适的子类</li></ul></li><li>xxx可能是一个子类<ul><li>如果这是一个子类，也有一种可能，这个子类只是实现一些特殊的对外接口，真正通用的干活核心是他的基类.init出来的基类实例。</li></ul></li></ul></li></ul></li></ul></li></ul></li></ul><p>所以，这些vllm里面有很多这种乱七八糟的类包含，继承的设计，简单总结下来有这几种：</p><ol><li>xxxProc()<ol><li>进程启动调用它的工厂函数来构造这个对象，来表示这个进程，已经这个进程的循环逻辑</li></ol></li><li>xxxWrapper()<ol><li>这个一般作为一个适配器的中间层，他的属性包含了我们需要的类实例，用来选择符合我们的类来构造</li></ol></li><li>xxxBase()<ol><li>这个类一般是用来作为我们真正需要的实例，有两种设计<ol><li>真的就是一个抽象的基类，比如定义一些基本的接口</li><li>作为核心功能的实现，子类仅仅只是包装点接口设置。</li></ol></li></ol></li></ol><hr><p>下面，我来按照主体的时间线，来进行梳理，从EngineCoreProc这个引擎后端进程开始，一直到Worker进程进入循环等待指令。详细梳理一下各个类实例的构建实际，已经每个进程是如何进入循环状态的。<br>之后，我将开始手写完成到Worker启动并进入循环的框架，之后我们就会进入ModelRunner，来真正接触模型。</p><p><strong>【引擎后端进程 EngineCoreProc 】</strong><br>    - (构造)CoreEngineProcManager.init<br>        - ….<br>        - 启动EngineCoreProc.run_engine_core()进程<br>            -(构造)EngineCoreProc（EngineCore）.init()<br>                - (创建)输入请求队列self.input_queue，输出请求队列self.output_queue<br>                -（保存）引擎编号 self.engine_index &#x3D; Dealer套接字identity<br>                - (过程) 和引擎前端的握手<br>                    - (保存) self.address &#x3D; address 记录前端的联系方式<br>                    - (构造)EngineCore.init()<br>                        - (构造执行器)：self.model_executor &#x3D; MultiprocExecutor.init()<br>                            - (构造) Executor.init()<br>                                - (调用) 调用子类的_init_executor()<br>                            - (调用) _init_executor()<br>                                - (计算) world_size &#x3D; tp x pp x pcp<br>                                - (调用) 设置worker的torch线程环境<br>                                - (计算) MultiprocExecutor 的 zmq 的 URL<br>                                - (构造) self.rpc_broadcast_mq : MessageQueue<br>                                - (计算) 连接执行器的信息：scheduler_output_handle<br>                                - (while)<br>                                    - (计算) 每个worker的rank<br>                                    - (调用) WorkerProc.make_worker_process()<br>                                        - (构造) 管道1:ready;  管道2:death<br>                                        - (启动新进程，WorkerProc.worker_main)              &gt;&gt;&gt;&gt;&gt;【Worker # N】<br>                                    - (保存) 把 unready_worker_handle 加入 unready_workers列表<br>                                - (阻塞调用)self.workers : List[ WorkerProcHandle ]&#x3D; WorkerProc.wait_for_ready(unready_workers) 等待所有Worker进程管道发生READY<br>                                - (创建) self.response_mqs 收集每一个worker的回复队列<br>                                - (调用) self.rpc_broadcast_mq.wait_until_ready() 验证发送通道的通路<br>                                - (调用) response_mq.wait_until_ready() 验证接受通道的通路<br>                                - (创建) future &#x3D; deque[FutureWrapper] 创建异步RPC的结果future FIFO队列<br>                                - (调用) success &#x3D; True， 执行器启动成功<br>                        - self.available_gpu_memory_for_kv_cache 记录kvcache的显存容量<br>                        - 【worker初始化第三步】self._initialize_kv_caches(vllm_config), （EngineCore让执行器RPC）让执行器RPC调用Worker初始化kvcache<br>                        - (构造) structured_output_manager &#x3D; StructuredOutputManager.init()<br>                        - (构造调度器) self.scheduler &#x3D; Scheduler.init()<br>                            - (保存) self.structured_output_manager &#x3D; structured_output_manager<br>                            - (保存) self.max_num_running_reqs 每轮最大请求数<br>                            - (保存) self.max_num_scheduled_tokens 每轮最大token数<br>                            - (保存) self.max_model_len 模型kvcache上下文窗口最大长度<br>                            - (保存) self.requests : dict 请求集合<br>                            - (保存) self.policy 调度策略<br>                            - (创建) self.waiting, self.skipped_waiting, self.running<br>                            - (创建) self.finished_req_ids, 上一轮完成的req集合<br>                            - (创建) self.reset_preempted_req_ids, 本轮被抢占的req 集合<br>                            - (构造) self.kv_cache_manager &#x3D; KVCacheManager() 显存块管理器<br>                            - (调用) self.connector, PD 分离用的<br>                            - (保存) self.current_step 调度步数计数器<br>                            - (保存) self._pause_state 调度器状态<br>                            - (保存) self._inflight_prefills 正在prefill的req的集合<br>                        - (构造)(远程kv) kv_connector, PD分离的通信管道<br>                        - (调用) 指定引擎self.step_fn() &#x3D; self.step()<br>                - (线程1)：输入线程process_input_sockets<br>                - (线程2)：输出线程process_output_sockets<br>            - (调用)EngineCoreProc.run_busy_loop()<br>                - 【loop】<br>                    - (调用) self._process_input_queue() 收集引擎后端的输入线程收集的ZMQ的请求，放入调度器队列<br>                    - (调用) self._process_engine_step() 引擎后端执行一步<br>                        - (调用) outputs, model_executed &#x3D; self.step_fn()，执行指定的方法<br>                        - (调用) 把outputs 放入output_queue</p><p><strong>【Worker - # N 进程-WorkerProc】</strong><br>    - WorkerProc.worker_main() 工厂函数<br>        - (保存) ready_writer, death_pipe （与引擎后端进程连接 的 管道）<br>        - (构造) worker &#x3D; WorkerProc.init()<br>            - (保存) self.rank &#x3D; rank<br>            - (构造) wrapper &#x3D; WorkerWrapperBase.init()<br>                - (声明) self.worker是WorkerBase类<br>            - (调用) wrapper.init_worker()<br>                - (构造) self.worker &#x3D; worker_class()(子类) &#x3D; Worker.init()<br>                    - (构造) WorkerBase.init()<br>                        - (声明) 定义抽象接口：<br>                            - 各种配置<br>                            - self.device : torch.device<br>                            - self.model_runner : nn.Module<br>                            - self.rank &#x3D; rank<br>                            - self.distributed_init_method (执行器的联系方式)<br>                    - (保存) 使用的model_runner 版本<br>                        - self.profiler : 测定初始参数<br>                        - self.profiler_config : 初始测定配置<br>            - (保存) self.worker &#x3D; wrapper<br>            - (调用) self.worker.init_device()<br>                - (转发) self.worker.init_device()<br>                    - 【worker初始化第一步】初始化设备 + 分布式上下文通信组 + model_runner(含InputBatch)<br>                        - (保存) parallel_config &#x3D; 并行化参数<br>                        - (计算) tp_pp_world_size 计算所需GPU个数<br>                        - (调用) 发布worker的逻辑rank id ~ GPU 映射表<br>                        - (调用) 设置pytorch的device为对应的GPU<br>                        - (调用) 拉起NCCL通信网络，做TP，PP，PCP，DP、EP、EPLB 组（EP&#x2F;EPLB 仅 MoE 模型）分组切分<br>                        - (调用) 设置随机种子<br>                        - (调用) gc.collect()，CPU侧清理垃圾变量内存<br>                        - (调用) torch.accelerator.empty_cache(), GPU侧清理垃圾变量显存<br>                        - (调用) 测量实际可以显存 vs 用户预先设置请求的内存空间<br>                        - (构造) 算子缓冲区的scratch草稿显存控制器 &#x3D; init_workspace_manager()<br>                            - 简单，待定<br>                        - (构造) self.model_runner &#x3D; GPUModelRunnerV1.init()<br>                            - …<br>            - (调用) self.worker.load_model()<br>                - (转发) 为空，转发Worker<br>                    - 【worker初始化第二步】构造模型结构 + 加载权重(按 TP&#x2F;PP 切分与设备放置) + model.eval() + 可选 graph包装<br>                        - (调用) 把vllm.config设为当前的vllm_config<br>                        - (调用) 调整cuda分配器的切片大小为20MB，用于分配权重显存<br>                        - (调用)self.model_runner.load_model() 加载权重<br>                            - (调用)实际测试设备内存<br>                            - (调用) model_loader &#x3D; get_model_loader() 获取模型加载器<br>                            - (调用) self.model &#x3D; model_loader.load_model() 加载模型<br>                            - (调用) 加载lora模型<br>                            - (调用) 加载草稿模型<br>                            - (调用) (可选) 包装一层cuda graph<br>        - (调用) ready_writer 发送 ready信号 + worker_response_mq的连接信息<br>        - (调用) rpc_broadcast_mq.wait_until_ready() 验证我们发送通路的正常，作为reader接收方<br>        - (调用) worker_response_mq.wait_until_ready() 验证我们回复通路的正常，作为writer发送方<br>        - (调用) worker.worker_busy_loop() Worker进程的RPC服务循环<br>            - 【loop】<br>                - (保存) rpc_broadcast_mq.dequeue，得到method + args + output_rank<br>                - (调用) 发给worker，调用method<br>                    - WorkerWrapperBase适配器命中实现<br>                    - WorkerWrapperBase适配器未命中<br>                        - Worker.func实现</p><p>WorkerProc<br>    - .worker : WorkerWrapperBase （适配器转发层）<br>        - .worker : Worker<br>          - .device : torch.device<br>          - .model_runner : nn.Module<br>            - .model<br>          - .rank &#x3D; rank<br>          - .distributed_init_method (执行器的联系方式)<br>          - .profiler<br>          - .profiler_config</p><h4 id="model-runner"><a href="#model-runner" class="headerlink" title="model_runner"></a>model_runner</h4><h5 id="xxx-config"><a href="#xxx-config" class="headerlink" title="xxx_config"></a>xxx_config</h5><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">VllmConfig</span>:</span><br><span class="line">model_config: ModelConfig <span class="comment"># 模型配置</span></span><br><span class="line">cache_config: CacheConfig <span class="comment"># kvcache的形状配置</span></span><br><span class="line">parallel_config: ParallelConfig <span class="comment"># 并行配置</span></span><br><span class="line">scheduler_config: SchedulerConfig <span class="comment"># 调度器配置</span></span><br><span class="line">device_cofnig:DeviceConfig <span class="comment"># 计算设备配置</span></span><br><span class="line">load_config <span class="comment"># 加载配置</span></span><br><span class="line">offload_config <span class="comment"># 模型权重卸载配置</span></span><br><span class="line">attention_cofnig <span class="comment"># 注意力配置</span></span><br><span class="line">kernel_config <span class="comment"># 内核配置</span></span><br><span class="line">speculative_config <span class="comment">#投机解码配置</span></span><br><span class="line">quant_config <span class="comment"># 量化配置</span></span><br><span class="line">profiler_config <span class="comment"># 测量配置</span></span><br><span class="line">kv_transfer_config <span class="comment"># pd分离的kv转移器配置</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ModelConfig</span>:</span><br><span class="line">model: <span class="built_in">str</span> <span class="comment"># hf的模型名称/路径</span></span><br><span class="line">model_weights: <span class="built_in">str</span> <span class="comment"># 原始模型权重地址</span></span><br><span class="line">runner <span class="comment"># model runner 的类型</span></span><br><span class="line">convert <span class="comment"># 使用adapters来转换模型，比如转换文本生成模型-&gt;池化任务</span></span><br><span class="line">tokenizer <span class="comment"># hf的分词器的名称/地址</span></span><br><span class="line">tokenizer_mode : TokenizerMode <span class="comment">#分词器的类型</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">分词器是**第三方库**，vLLM 不自己实现 tokenization，只是加载/包装。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">具体是哪个，由 `tokenizer_mode` 决定：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- **`hf` / `auto`**：Hugging Face 的 `transformers.AutoTokenizer`，底层是 HF 的 Rust 库 `tokenizers`（fast tokenizer）。</span></span><br><span class="line"><span class="string">- **`mistral`**：`mistral_common`（Mistral 自家的库）。</span></span><br><span class="line"><span class="string">- **`deepseek_v32/v4`**：DeepSeek 自家的 tokenizer。</span></span><br><span class="line"><span class="string">- **`cohere`**：`cohere_melody` 库。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">`tokenizer` 字段只是**指定从哪加载**（模型名或本地路径），vLLM 据此去 Hugging Face 仓库 / 本地目录里找 tokenizer 配置文件。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">所以 tokenizer 是外部依赖，vLLM 调用它做 text ↔ token id 的转换（编码 prompt、解码输出 token），本身只负责调度和管理。</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line">dtype <span class="comment"># 模型权重/激活值的数据类型</span></span><br><span class="line">seed <span class="comment"># 随机种子，我们必须使用全局一样的随机种子，不然TP会导致不一致的采样结果</span></span><br><span class="line"></span><br><span class="line">spec_target_max_model_len <span class="comment"># 投机解码的最大长度</span></span><br><span class="line">quantization <span class="comment"># 量化方式</span></span><br><span class="line">quantization_config <span class="comment"># 量化配置</span></span><br><span class="line"></span><br><span class="line">hf_config <span class="comment"># 模型的huggingface配置</span></span><br><span class="line">hf_config_path <span class="comment"># 配置地址，未指定就直接用模型的目录</span></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 class="keyword">class</span> <span class="title class_">LoadConfig</span>:</span><br><span class="line">load_format: <span class="built_in">str</span> <span class="comment"># 模型权重的加载格式</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">auto: 默认用safetensors格式加载权重 </span></span><br><span class="line"><span class="string">safetensors:</span></span><br><span class="line"><span class="string">instanttensor:</span></span><br><span class="line"><span class="string">npcache</span></span><br><span class="line"><span class="string">dummy: # 用随机值初始化权重，主要用来profiling</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line">download_dir:<span class="built_in">str</span> <span class="comment"># 下载、加载目录，默认是huggingface的缓存目录</span></span><br><span class="line">safetensors_load_strategy <span class="comment"># 加载safetensors权重的策略，</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">默认：使用mmap懒加载</span></span><br><span class="line"><span class="string">lazy: 权重被mmap，按需加载</span></span><br><span class="line"><span class="string">eager: 加载前，把所有权重读到ddr，NFS推荐</span></span><br><span class="line"><span class="string">prefetch: checkpoint 文件被读入OS页缓存，在加载之前，网络，高延迟存储适用</span></span><br><span class="line"><span class="string">torchao: 读完重建为 **torchao 量化张量**</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line">safetensors_prefetch_num_threads <span class="comment"># 张量预取线程数</span></span><br><span class="line"></span><br><span class="line">device <span class="comment"># 把模型权重加载到哪个设备上，默认device_config.device</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CacheConfig</span>:</span><br><span class="line">block_size <span class="comment">#（默认 16）— 一个 block 装多少 token</span></span><br><span class="line">num_gpu_blocks / num_cpu_blocks <span class="comment"># profiling 后回填的实际块数（用户不设，init=False）</span></span><br><span class="line">num_gpu_blocks_override <span class="comment"># 手动覆盖 profiling 算出的块数</span></span><br><span class="line">gpu_memory_utilization <span class="comment"># （默认 0.92）— 显存利用率</span></span><br><span class="line">kv_cache_memory_bytes <span class="comment">#手动指定 KV cache 字节数（覆盖上面的利用率）</span></span><br><span class="line"></span><br><span class="line">cache_dtype <span class="comment">#  KV cache 存储 dtype（auto/fp8/bf16…）</span></span><br><span class="line">kv_cache_dtype_skip_layers <span class="comment"># 跳过量化的层</span></span><br><span class="line"></span><br><span class="line">enable_prefix_caching <span class="comment"># prefix_match_unit</span></span><br><span class="line">prefix_caching_hash_algo <span class="comment">#哈希算法（sha256/xxhash…）</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260816150138.png"><br><img src="/images/Pasted%20image%2020260816150321.png"></p><h5 id="GPUModelRunner"><a href="#GPUModelRunner" class="headerlink" title="GPUModelRunner"></a>GPUModelRunner</h5><p>下面来整理一下GPUModelRunner类的基本属性</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">GPUModelRunner</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line"><span class="comment"># 配置信息</span></span><br><span class="line">vllm_config <span class="comment">#推理框架配置</span></span><br><span class="line">model_config <span class="comment">#运行模型的信息</span></span><br><span class="line">cache_config <span class="comment">#kvcache规格配置</span></span><br><span class="line">offload_config <span class="comment">#模型卸载器配置</span></span><br><span class="line">load_config <span class="comment"># 模型加载器配置</span></span><br><span class="line">parallel_config <span class="comment"># 并行加速配置</span></span><br><span class="line">scheduler_config <span class="comment"># 调度器配置</span></span><br><span class="line">speculative_config <span class="comment"># 投机解码配置</span></span><br><span class="line"></span><br><span class="line">device <span class="comment">#运行设备</span></span><br><span class="line">dtype  <span class="comment">#数据运行精度</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 执行信息</span></span><br><span class="line">max_num_tokens <span class="comment">#一个batch里面，最大tokens计算数</span></span><br><span class="line">max_num_reqs <span class="comment">#一个batch里req的最大数量</span></span><br><span class="line">num_query_heads <span class="comment">#每个req的query部分的头数 = 注意力头数</span></span><br><span class="line">inputs_embeds_size  <span class="comment"># ?</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 采样器</span></span><br><span class="line"><span class="variable language_">self</span>.sampler <span class="comment"># 采样器</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># kvcache相关属性</span></span><br><span class="line"><span class="variable language_">self</span>.kv_caches : <span class="built_in">list</span>[torch.Tensor] <span class="comment"># 各layer的kvcache tensor 列表</span></span><br><span class="line"><span class="variable language_">self</span>.attn_groups : <span class="built_in">list</span>[<span class="built_in">list</span>[AttentionGroup]] <span class="comment">#[kv_cache_group_id][attn_backend_group]</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 请求状态</span></span><br><span class="line"><span class="variable language_">self</span>.requests : <span class="built_in">dict</span>[req_name, CacheRequestState] <span class="comment">#历史req的信息</span></span><br><span class="line"><span class="variable language_">self</span>.input_batch : InputBatch <span class="comment"># 输入批实例</span></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 class="comment"># 单轮req缓冲区</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">这一块的缓冲区的申请，主要用于存放，从本轮batch的调度任务中提取的有用信息，以及和上一次的batch有什么差异。</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="variable language_">self</span>.input_ids <span class="comment"># gpu-cpu双缓冲buffer, 长度=单batch最大可计算tokens数，新计算token的缓冲区</span></span><br><span class="line"><span class="variable language_">self</span>.req_indices <span class="comment"># gpu-cpu双缓冲buffer,长度=单batch最大可计算tokens数，token-&gt;req_id的映射表</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.positions <span class="comment"># gpu侧张量，长度=单batch最大可计算tokens数，每个token在req中的pos</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.query_start_loc <span class="comment"># gpu-cpu双缓冲buffer, 长度=最大请求数+1，每个req的开始计算的pos</span></span><br><span class="line"><span class="variable language_">self</span>.prev_num_draft_tokens <span class="comment"># gpu-cpu双缓冲buffer, 长度=最大请求数，每个req的草稿token数</span></span><br><span class="line"><span class="variable language_">self</span>.prev_positions <span class="comment"># gpu-cpu双缓冲buffer, 长度=最大请求数，前后两次batch的req的顺序变动映射</span></span><br><span class="line"><span class="variable language_">self</span>.num_scheduled_tokens <span class="comment"># gpu-cpu双缓冲区，长度=最大请求数，每个请求本次要计算的token数</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.seq_lens <span class="comment"># gpu 侧张量，长度=最大请求数， 用来记录每个req的长度，每个req的已有kv的token长度 + 本轮即将计算的token的序列长度</span></span><br><span class="line"><span class="variable language_">self</span>.num_computed_tokens <span class="comment"># gpu侧张量，长度=最大请求数，每个req已有kv的token长度。</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下面来先看一下，我们model_runner保存的每一个request的历史类CachedRequestState:<br>里面有哪些内容</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CachedRequestState</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line">req_id : <span class="built_in">str</span>  <span class="comment"># 请求id</span></span><br><span class="line">prompt_token_ids : <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># prompt的token id列表</span></span><br><span class="line">sampling_params : SamplingParams <span class="comment"># 采样参数</span></span><br><span class="line">generator : torch.Generator <span class="comment"># PyTorch 随机数生成器, 给采样器从打分里面采样用的</span></span><br><span class="line"></span><br><span class="line">block_ids : <span class="built_in">tuple</span>[<span class="built_in">list</span>[<span class="built_in">int</span>], <span class="built_in">list</span>[<span class="built_in">int</span>], ...] <span class="comment"># 该req的各group的kv block的列表</span></span><br><span class="line">num_computed_tokens : <span class="built_in">int</span> <span class="comment"># 已经计算的token数</span></span><br><span class="line">output_tokens_ids : <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># decode输出的token id列表</span></span><br></pre></td></tr></table></figure><h6 id="InputBatch"><a href="#InputBatch" class="headerlink" title="InputBatch"></a>InputBatch</h6><p>下面我们再来看一下InputBatch类实例里面有哪些重要的属性</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">InputBatch</span>:</span><br><span class="line"><span class="comment"># 基础属性</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 基础配置</span></span><br><span class="line">max_num_reqs <span class="comment"># 一个batch的最大req数</span></span><br><span class="line">max_model_len <span class="comment"># 每个req的kvcache最大上下文长度</span></span><br><span class="line">max_num_batched_tokens  <span class="comment"># 每个batch中本次最大计算token数</span></span><br><span class="line">device     <span class="comment"># 设备</span></span><br><span class="line">vocab_size <span class="comment"># 词袋大小</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 【batch内-req_id 的双向索引表】</span></span><br><span class="line">_req_ids : <span class="built_in">list</span>[<span class="built_in">str</span>] <span class="comment"># 一个batch里面的req id 列表(正向batch内索引)</span></span><br><span class="line">req_id_to_index : <span class="built_in">dict</span>[req_id, <span class="built_in">int</span>] </span><br><span class="line"><span class="comment">#req_id → 它在当前 batch 里的位置索引（反向batch内索引）</span></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 class="comment"># 【token id 信息】</span></span><br><span class="line"><span class="comment"># 【每个req的完整token ids序列】的大缓冲区</span></span><br><span class="line">token_ids_cpu_tensor <span class="comment"># 张量（req_id, max_model_len），</span></span><br><span class="line"><span class="comment"># CPU 侧按「[req, 序列长度]」二维存每个请求完整 token 序列的大缓冲</span></span><br><span class="line">token_ids_cpu <span class="comment"># token_ids_cpu_tensor的数组格式</span></span><br><span class="line">is_token_ids_tensor <span class="comment"># 每个位置是不是「真 token id」的布尔掩码，</span></span><br><span class="line"><span class="comment"># 形状和 `token_ids_cpu_tensor` 一样</span></span><br><span class="line">req_prompt_embeds : <span class="built_in">dict</span>[req_id, Tensor] <span class="comment"># 直接以prompt的embedding向量形式的输入</span></span><br><span class="line">req_output_token_ids <span class="comment"># 每个req已生成的output token id序列</span></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 class="comment"># 【token数统计信息】</span></span><br><span class="line"><span class="comment"># 【每个req的各种数量统计】</span></span><br><span class="line">num_tokens_no_spec <span class="comment"># (req_id,), 每个req不含投机解码token数</span></span><br><span class="line"><span class="comment"># 序列里 真实存在 的 token 总数（prompt + 接受的输出），「序列有多长」</span></span><br><span class="line">num_prompt_tokens <span class="comment"># (req_id,) 每个req的prompt长度</span></span><br><span class="line">num_computed_tokens_cpu <span class="comment"># (req_id, ) 每个req已有kvcache的token数，决定这次从哪里开始</span></span><br><span class="line"><span class="comment"># 模型前向已算的 token 数（KV 进度），「算到哪了」</span></span><br><span class="line"></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">num_tokens, 和 num_computed_tokens的区别：</span></span><br><span class="line"><span class="string">num_tokens是说目前req里已经知道这个token id的数量，不管算没算kv</span></span><br><span class="line"><span class="string">这个里面包含发过来的prefill阶段的prompt的req;</span></span><br><span class="line"><span class="string">投机解码时候的，草稿也被算到这个num_tokens里面了。</span></span><br><span class="line"><span class="string">num_computed_tokens 是说，这个token id已经存在的里面，已经有kvcache的。</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></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 class="comment"># 【每个req 的 单block_id 信息 +  block_id - 物理kv地址 信息（现用现算）】</span></span><br><span class="line"><span class="comment"># 【block - 物理地址】页表</span></span><br><span class="line">block_table = MultiGroupBlockTable() <span class="comment"># block table，每个req的kvcache blodk id映射，</span></span><br><span class="line"><span class="comment"># 前向推理时，attention靠它把逻辑块id转成物理kv地址。</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">    列 = 块位置 0   1   2   3  ...</span></span><br><span class="line"><span class="string">行 req0 [ block_id 12, 7, 33, 0, ... ]   ← 这就是 req0 的 block id 链</span></span><br><span class="line"><span class="string">行 req1 [ block_id 5, 18, 0, 0, ... ]</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">-----------------------------------------------------------------------------</span></span><br><span class="line"><span class="string">第二层要修正：slot_mapping 管「写」，注意力「读」不经过它</span></span><br><span class="line"><span class="string">KV cache 有两条路径，都要「block id → 物理地址」，但方式不同：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">1. 写路径（当前 token 的 K/V 存进去）</span></span><br><span class="line"><span class="string">- compute_slot_mapping（GPU kernel）把 block_table + block_size 算成 slot_mapping（每 token 一个写地址）</span></span><br><span class="line"><span class="string">- 喂给 `concat_and_cache` kernel，把当前 token 的 K/V 写进对应物理槽</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">2. 读路径（注意力读历史 KV）</span></span><br><span class="line"><span class="string">- PagedAttention kernel 直接用 block_table + seq_lens，在 kernel 内部按块迭代，每块现场算 `block_id × block_size` 得到物理偏移</span></span><br><span class="line"><span class="string">- 不经过 slot_mapping</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">--------------------------------------------------------------------------</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">compute_slot_mapping 的作用 = 并行把「每 token 属于哪个 block、块内哪个位置」解算成「每 token 的物理 slot 地址」</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">slot_mapping[token_i] = block_id × block_size + 块内偏移</span></span><br><span class="line"><span class="string">slot_mapping 是个一维 int64 张量，长度 = max_num_batched_tokens（每 token 一个，不是每 req、也不是每 block）。每个元素 = 该 token 的 KV 要写入的物理 slot 地址</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -------------------------------------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 采样策略</span></span><br><span class="line">temperature <span class="comment"># （req_id, ），每个req的采样温度</span></span><br><span class="line">greedy_reqs : <span class="built_in">set</span>[req_id]<span class="comment">#  贪心采样的req集合</span></span><br><span class="line">random_reqs : <span class="built_in">set</span>[req_id] <span class="comment"># 随机采样的req集合</span></span><br><span class="line"></span><br><span class="line">top_p <span class="comment"># 核采样，每个req一个</span></span><br><span class="line">top_k <span class="comment"># top_k采样</span></span><br><span class="line"></span><br><span class="line">frequency_penalties <span class="comment"># 频率惩罚</span></span><br><span class="line">presence_penalties <span class="comment"># 存在惩罚</span></span><br><span class="line">repetition_penalities <span class="comment"># 重复惩罚</span></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 class="comment"># 投机解码</span></span><br><span class="line">num_accepted_tokens_cpu <span class="comment"># 各req 在本步 被接受的 草稿token数，初始=1，不使用投机解码就是1token</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 每步状态</span></span><br><span class="line"><span class="variable language_">self</span>.batch_update_builder = BatchUpdateBuilder()</span><br><span class="line"><span class="comment"># 每步batch状态变化的内部表示，用于重排持久batch 和 生成 logitsprocs的状态更新。每步重置</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 限制词</span></span><br><span class="line">has_allowed_token_ids  <span class="comment"># 限制了 “只允许输出某些token” 的 req集合</span></span><br><span class="line">allowed_token_ids_mask <span class="comment"># 允许token的掩码矩阵，允许为false</span></span><br><span class="line">bad_words_token_ids <span class="comment">#req_id -&gt; 禁用词列表</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成相关杂项</span></span><br><span class="line"></span><br><span class="line">spec_token_ids <span class="comment"># 每个req上一轮的草稿token列表</span></span><br><span class="line">logitsprocs <span class="comment"># 自定义的logits处理器集合（打分处理器集合）</span></span><br><span class="line">sampling_metadata <span class="comment">#采样元数据（打包所有采样参数，生成器，打分处理器，喂给采样器），每次batch组成变化时重组</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">prev_req_id_to_index <span class="comment"># 上一批的req_id -&gt; 位置映射</span></span><br><span class="line"></span><br><span class="line">sampled_token_ids_cpu <span class="comment"># 上一批真实采样token的CPU缓存，供当前采样参数需要（如penalty）时更新output_token_ids</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">### logprob = log probability 对数概率。即模型给 某个token 打的对数概率值</span></span><br><span class="line"><span class="comment"># logits(原始分数)-》 softmax-&gt;概率p-&gt;取对数-》logprob</span></span><br></pre></td></tr></table></figure><blockquote><p>InputBatch实例，<mark style="background:#fff88f">维护一个 【持久化的 在途请求 状态】</mark>，跨step存活</p><p>这个实例，是model_runner的一个成员属性，核心是用【槽位req_index】组织一批【当前正在这个GPU worker上跑的请求】。槽位状态跨多步保留<br><img src="/images/Pasted%20image%2020260817212901.png"><br>因此，每步只做增量更新，不重建<br><img src="/images/Pasted%20image%2020260817213003.png"></p></blockquote><p>所以，实际的数据流程是这样的：</p><ol><li>SchedulerOutput (调度器发过来的req batch)</li><li>-&gt; InputBatch实例， <code>_update_states</code> 增量更新 <ol><li>加新的req, 删除完成的，重排、压缩槽位</li></ol></li><li>-&gt; <code>_preprocess</code> 从InputBatch 持久化状态中，填写我们的当前步的buffer<ol><li>input_ids, positions, seq_lens, query_start_loc, slot_mapping</li></ol></li><li>-&gt; <code>_model_forward</code> + <code>_sample</code> 前向 + 采样</li><li>-&gt; <code>_bookkeeping_sync</code> 把结果写会InputBatch<ol><li>output_token_ids 追加，num_computed_tokens + 1, generator 状态更新</li></ol></li><li>下一轮 SchedulerOutput</li></ol><p>下面我整理了一下，我们执行一个req的主要的信息，以及各个模块持有这些信息关系图：</p><p>首先总结一下，一个req，到底有哪些信息：</p><ul><li><mark style="background:#d3f8b6">token ids 信息</mark><ul><li>这个是token id列表</li></ul></li><li><mark style="background:#d3f8b6">token 数 统计信息</mark><ul><li>num_tokens, num_computed_tokens, num_prompt_tokens</li></ul></li><li><mark style="background:#d3f8b6">token_id - 内容hash链</mark></li><li><mark style="background:#d3f8b6">内容hash链 - block_id 信息</mark></li><li><del>block_id信息 - 物理kv地址</del></li><li><mark style="background:#d3f8b6">单block_id信息</mark><ul><li>物理kv地址，可以通过block_id的值，就可以推算出来了<ul><li>写kvcache, 通过compute_slot_mapping， 来计算每个token的物理地址</li><li>读kvcache，直接在注意力算子里面，计算每个需要的seq_len个token个物理地址。</li></ul></li></ul></li></ul><p>然后我梳理了一下各个类，看看里面的主要的属性，搞清楚各个模块持有的信息，就可以搞清楚各个模块的功能了：</p><ul><li><mark style="background:#fff88f">Request实例</mark><ul><li>token ids信息</li><li>token 数统计信息</li><li>token_id - 内容hash链</li></ul></li></ul><blockquote><p>Request实例，经过kvcachemanager, 以及scheduler里的方法，提取出增量信息</p></blockquote><ul><li><mark style="background:#fff88f">NewRequestsData实例</mark><ul><li>token ids 信息</li><li>token数统计信息</li><li>req 的 单block_id 信息 （prompt）</li></ul></li><li><mark style="background:#fff88f">CacheRequestData</mark><ul><li>token 数统计信息</li><li>req 的 单block_id 信息 (decode)</li></ul></li></ul><p>整个KVCacheManger, KVCacheCoordinator, 里面，都只是关于维护block pool的数据结构。</p><p>相关的在KVCacheBlock这个一个块的类实例里面</p><ul><li><mark style="background:#fff88f">KVCacheBlock实例</mark><ul><li>内容hash链 - block_id 信息</li></ul></li></ul><p>所以vllm把hash链做在block内，所以一个request只要持有block_id即可。</p><p>下面看一下InputBatch里面</p><ul><li><mark style="background:#fff88f">InputBatch实例</mark><ul><li>token ids 信息</li><li>token 数统计信息</li><li>req的单block_id信息</li></ul></li></ul><hr><p><mark style="background:#d2cbff">所以至此，一个req的一开始,只有3个核心信息：</mark></p><ul><li><mark style="background:#d3f8b6">token_ids 信息</mark></li><li><mark style="background:#d3f8b6">tokens 数统计信息</mark></li><li><mark style="background:#d3f8b6">block_ids 信息</mark><ul><li>hash写在KVCacheBlock块里面，所以和block_id匹配</li><li>block_id的值和物理地址绑定</li></ul></li></ul><p>每个Request实例，构建的时候，拥有前两个+hash</p><ul><li>经过KVCacheManager， 把hash信息换成了block_id信息</li></ul><p><strong>所以NewRequestsData, CacheRequestsData实例里面，已经拥有了这三个核心增量信息</strong>。</p><p>然后打包成SchedulerOutput实例发出去，给InputBatch实例，进行<strong>更新目前信息，合并增量信息</strong></p><p>最后InputBatch实例，留下来的就是完整的当前GPU要处理的：</p><ul><li>token_ids 信息 （完整）</li><li>tokens 数统计信息 （完整）</li><li>block_ids 信息 （完整）<blockquote><p>增加请求，被抢占了，执行完了，InputBatch就移除请求</p></blockquote></li></ul><p><img src="/images/Pasted%20image%2020260817222257.png"></p><h4 id="总结：模型的构建和加载权重，初始化kvcache-流程梳理"><a href="#总结：模型的构建和加载权重，初始化kvcache-流程梳理" class="headerlink" title="总结：模型的构建和加载权重，初始化kvcache 流程梳理"></a>总结：模型的构建和加载权重，初始化kvcache 流程梳理</h4><p>这边来梳理一下worker的主要工作：</p><ul><li>构造模型加载器</li><li>构造模型实例，加载模型权重</li><li>profiling 测试kvcache的实际可用显存</li><li>初始化kvcache显存张量</li></ul><p>下面我将通过顺序一步一步，从worker的构造开始</p><p>【EngineCore进程】</p><ul><li>EngineCoreProc.run_engine_core()<ul><li>(构造)EngineCoreProc.init()，<mark style="background:#b1ffff">引擎后端的功能 + 通信</mark><ul><li>(创建) EngineCoreProc.input_queue</li><li>(创建) EngineCoreProc.output_queue</li><li>(保存) EngineCoreProc.engine_index</li><li>(with) 与前端握手通信<ul><li>(构造) 基类 EngineCore.init， <mark style="background:#affad1">构造引擎后端的功能实例</mark><ul><li>(构造) EngineCore.self.model_executor &#x3D; MultiprocExecutor.init()， <mark style="background:#fff88f">构造执行器</mark><ul><li>(创建) MultiprocExecutor.rpc_broadcast_mq()，发送通道</li><li>(while)<ul><li>(调用) WorkerProc.make_worker_process, 创建，启动worker进程<ul><li>(创造) ready 管道</li><li>(创造) death管道</li><li>(创建) 【<mark style="background:#fdbfff">Worker进程】》》》》》 WorkerProc.worker_main()</mark></li></ul></li></ul></li><li>(调用) <font color="#ffc000">MultiprocExecutor.self.workers</font> &#x3D; WorkerProc.wait_for_ready, 阻塞等待worker进程全部启动成功</li><li>(保存) MultiprocExecutor.response_mqs 收集回复队列</li></ul></li><li>(调用) EngineCore.<code>_initialize_kv_caches(vllm_config)</code> <mark style="background:#fff88f">初始化kvcache显存</mark><ul><li>(保存) 获取kv_cache_spec规格</li><li>(调用) EngineCore.model_executor.determine_available_memory()让执行器 <mark style="background:#b1ffff">做profile探测kvcache可用显存空间</mark><ul><li>(RPC) <mark style="background:#fdbfff">》》》》》》》》》发送determine_available_memory任务</mark></li></ul></li><li>(保存) 根据探测结果，更新kv_cache_configs</li><li>(调用) EngineCore.model_executor.initialize_from_config 开始<mark style="background:#b1ffff">让执行器初始化kvcache显存区域</mark><ul><li>(RPC)<mark style="background:#fdbfff">》》》》》》》》》发生initialize_from_config任务</mark></li></ul></li></ul></li><li>(构造) EngineCore.self.scheduler &#x3D; Scheduler.init()， <mark style="background:#fff88f">构造调度器</mark></li><li>(调用) EngineCore.step_fun &#x3D; self.step()，<mark style="background:#fff88f">指定单步运行方法</mark></li></ul></li></ul></li></ul></li><li>(调用) EngineCoreProc.run_busy_loop()</li></ul></li></ul><p>【worker进程】</p><ul><li><mark style="background:#fdbfff">【Worker进程开始】WorkerProc.worker_main()</mark><ul><li>(构造) WorkerProc.init()<ul><li>(保存) self.rank &#x3D; rank  分布式通信的卡号</li><li>(构造) wrapper &#x3D; WorkerWrapperBase() <mark style="background:rgba(92, 92, 92, 0.2)">构造Worker调用适配器实例</mark></li><li>(调用) wrapper.<mark style="background:#fff88f">init_worker() </mark>， 开始<mark style="background:#fff88f">构造Worker实例</mark><ul><li>(构造 )WorkerWrapperBase.worker &#x3D; Worker.init()<ul><li>(构造) WorkerBase.init()<ul><li>(保存) 保存配置文件</li><li>(声明) WorkerBase.self.device 设备是cuda</li><li>(声明) WorkerBase.model_runner : nn.Module</li></ul></li><li>(保存) 各种保存，记录使用model_runner的版本</li></ul></li></ul></li><li>(保存) WorkerProc.self.worker &#x3D; wrapper</li><li>(调用) WorkerProc.self.worker.<mark style="background:#fff88f">init_device()</mark><ul><li>(调用) 设置设备是CUDA + NCCL通信组构建+创建内存拍照器</li><li>(构造) Worker.self.model_runner &#x3D; GPUModelRunnerV1.init(), <mark style="background:#d3f8b6">构造model_runner</mark><ul><li>(保存) 保存各种配置config</li><li>(构造) GPUModelRunner.self.sampler &#x3D; Sampler.init() <mark style="background:#b1ffff">构造采样器</mark></li><li>(声明) GPUModelRunner.self.kv_caches : list[torch.Tensor] <mark style="background:#b1ffff">声明占位各kvcache tensor列表</mark></li><li>(创建) GPUModelRunner.self.requests 持久化请求状态记录</li><li>(创建) GPUModelRunner.self.input_batch &#x3D; InputBatch.init, 构建输入格式转换器</li><li>(创建) 每轮batch输入的整理统计信息 的 缓冲区申请</li></ul></li></ul></li><li>(调用) WorkerProc.self.worker.<mark style="background:#fff88f">load_model()</mark><ul><li>(构造) GPUModelRunner.get_model_loader(load_config), <mark style="background:#d3f8b6">构建模型加载器</mark><ul><li>返回一个DefaultModelLoader(BaseModelLoader) 实例</li></ul></li><li>(创造) GPUModelRunner.self.model &#x3D; BaseModelLoader.load_model()  <mark style="background:#d3f8b6">加载模型+权重拷贝</mark><ul><li>(调用) <mark style="background:#b1ffff">指定torch的设备</mark> &#x3D; cuda</li><li>(调用) model &#x3D; initialize_model() <mark style="background:#b1ffff">构造Qwen2ForCausalLM 模型实例</mark><ul><li>(调用) 从 model_config中<mark style="background:#fdbfff">解析出模型类 model_class</mark></li><li>(调用) 获取这个类的初始化参数列表</li><li>(构造) model &#x3D; Qwen2ForCausalLM.init() <mark style="background:#fdbfff">构造模型实例</mark></li><li>(调用) return model 返回模型实例</li></ul></li><li>(调用) DefaultModelLoader.load_weights(model, model_config) <mark style="background:#b1ffff">加载模型权重</mark><ul><li>(调用) DefaultModelLoader.get_all_weights <mark style="background:#d4b106">返回safetensors的权重迭代器</mark><ul><li>指定model_config.model， yield from <code>_get_weights_iterator</code><ul><li>weights_iterator &#x3D; safetensors_weights_iterator <mark style="background:#d2cbff">判定到是safetensors格式</mark>的<ul><li>safe_open，pt格式，<mark style="background:rgba(173, 239, 239, 0.55)">使用mmap懒加载，yield抛出一个个权重tensor</mark></li></ul></li></ul></li></ul></li><li>(调用) Qwen2ForCausalLM.load_weights(权重数据迭代器)<ul><li>AutoWeightsLoader.load_weights(迭代器)<ul><li>迭代器抛出的权重长这样：</li><li><img src="/images/Pasted%20image%2020260817105357.png" alt="384"></li><li>把weights迭代器的每一条权重，打包成(name, data)</li><li>(调用) AutoWeightsLoader.<code>_load_module()</code>， 开始<mark style="background:#b1ffff">递归匹配模型的张量参数 《-》weights里面的张量数据</mark><ul><li>child_modules 为 内部子模块</li><li>child_params 为 内部的张量参数</li><li>(调用) <code>_load_param</code> <mark style="background:#b1ffff">递归到叶节点</mark>，开始加载这个权重数据到这个权重参数<ul><li>(调用) default_weight_loader(param, weight_data)， 实际的拷贝器函数<ul><li><mark style="background:#d4b106"> param.data.copy_(loaded_weight)，利用张量类型的data.copy_来触发拷贝，然后触发page fault，从磁盘-&gt;cpu-&gt;gpu</mark></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li>(调用) WorkerProc.worker_busy_loop()<ul><li><p>接受RPC并执行</p></li><li><p><mark style="background:#fdbfff">》》》》》》determine_available_memory 任务</mark></p><ul><li>(调用) model_runner.profile_run()<ul><li>(调用) GPUModelRunner.<code>_dummy_run()</code><ul><li>(计算) 指定dummy batch的规格，让缓冲区达到峰值</li><li>(调用) 创建kvcache槽位，self.<code>_get_slot_mappings</code></li><li>(调用) <mark style="background:#d3f8b6">outputs &#x3D; self.model() 前向推理</mark></li></ul></li></ul></li><li>(执行) 计算可用kvcache 显存字节量<ul><li>(保存) self.available_kv_cache_memory_bytes &#x3D; self.requested_memory - profile_result.non_kv_cache_memory-cudagraph评估</li></ul></li></ul></li><li><p><mark style="background:#fdbfff">》》》》》》initialize_from_config任务</mark></p><ul><li>(保存) 保存 cache_config.num_gpu_blocks</li><li>(调用) model_runner.initialize_kv_cache(kv_cache_config) 开始初始化显存，构造每层的kvcachetensor<ul><li>(调用) kv_caches &#x3D; self.GPUModelRunner.initialize_kv_cache_tensors() 划分tensor<ul><li>(调用) kv_cache_raw_tensors &#x3D; self.<code>_allocate_kv_cache_tensors</code> 划分tensor</li><li>(调用) kv_caches &#x3D; self.<code>_reshape_kv_shape_tensors</code> 把每个tensor， reshape出block维度。</li></ul></li></ul></li></ul></li></ul></li></ul></li></ul><hr><h4 id="梳理调度任务-inputbatch-缓冲区-模型输入的数据流转"><a href="#梳理调度任务-inputbatch-缓冲区-模型输入的数据流转" class="headerlink" title="梳理调度任务-inputbatch-缓冲区-模型输入的数据流转"></a>梳理调度任务-inputbatch-缓冲区-模型输入的数据流转</h4><p>先整理一下GPUModelRunner里面所有出现的描述我们的请求的变量，给他们归类整理一下</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">GPUModelRunner</span>:</span><br><span class="line"><span class="comment"># init</span></span><br><span class="line">max_num_tokens <span class="comment"># 本轮最大要计算的tokens数</span></span><br><span class="line">max_num_reqs   <span class="comment"># 本轮的最大req数</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.kv_caches : <span class="built_in">list</span>[torch.Tensor] <span class="comment"># list[layer_i层的kvcache tensor]</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">#————————————————————————————————————————————————————————————————</span></span><br><span class="line"><span class="variable language_">self</span>.requests : <span class="built_in">dict</span>[req_id, CacheRequestState] <span class="comment"># 持久化状态对象库</span></span><br><span class="line"><span class="variable language_">self</span>.input_batch <span class="comment"># 持久化状态对象池</span></span><br><span class="line"><span class="comment">#----------------------------------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.input_ids <span class="comment"># [token_0, token_1, .....], 长度=max_num_tokens</span></span><br><span class="line"><span class="variable language_">self</span>.positions <span class="comment"># [pos_0, pos_1, ......], 长度=max_num_tokens</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.query_start_loc <span class="comment">#[l_0, l_1, ......, l_n], 长度=max_num_reqs+1</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.seq_lens <span class="comment"># [req_0_len, req_1_len, ....], 长度=max_num_reqs</span></span><br><span class="line"><span class="variable language_">self</span>.num_computed_tokens <span class="comment"># [num_0, num_1, ....], 长度=max_num_reqs</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.prev_num_draft_tokens <span class="comment">#[num_draft_0, num_draft_1, ...], 长度=max_num_reqs</span></span><br><span class="line"><span class="variable language_">self</span>.req_indices <span class="comment">#[req_x, req_x, req_y, ...], 长度=max_num_tokens</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.prev_positions <span class="comment"># [last_pos_req0, last_pos_req1, ...], 长度=max_num_reqs</span></span><br><span class="line"><span class="variable language_">self</span>.num_scheduled_tokens <span class="comment"># [num_0, num_1, ...], 长度=max_num_reqs</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.inputs_embeds <span class="comment">#[embed_size_token_0, embed_size_token_1, ...], 长度=max_num_tokens</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.is_token_ids <span class="comment"># [1,1, 0, 0,....], 长度=max_num_tokens</span></span><br><span class="line"><span class="variable language_">self</span>.num_decode_draft_tokens <span class="comment">#[num_0, num_1,....], 长度=max_num_reqs</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.num_accepted_tokens <span class="comment">#[num_0, num_1,...], 长度=max_num_reqs</span></span><br><span class="line"><span class="variable language_">self</span>.query_pos <span class="comment"># [p1, p2, p3, q1,q2,...], 一维token ids中，每个toekn在各自req内query的位置</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">execute_model</span>:</span><br><span class="line">slot_mappings <span class="comment"># 写token 的 kvcache， 各个token的kv地址列表</span></span><br><span class="line">  <span class="comment"># 读block 的 kvcache, 直接attention算子来</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">_prepare_inputs</span>:</span><br><span class="line">req_indices</span><br><span class="line">token_indices</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下面来开始整理<br><img src="/images/Pasted%20image%2020260818234616.png" alt="357"><br><img src="/images/Pasted%20image%2020260818235154.png"></p><p>所以建立在这个的基础上，你可以看到，这些用_make_buffer创建出来的缓冲区，其实就是对本轮调度的一个一维化罢了</p><p>关于要不要填充</p><ul><li>input_ids 是未填充的，用来取要计算的token</li><li>token_indices_tensor，是这个input_ids对应位置token在填充后的索引</li><li>token_ids_cpu_tensor, 这个就是完整的填充后的一维存放<br><img src="/images/Pasted%20image%2020260819144647.png"><br><img src="/images/Pasted%20image%2020260819144810.png"><br><img src="/images/Pasted%20image%2020260819144850.png"></li></ul><h3 id="各种并行化方案"><a href="#各种并行化方案" class="headerlink" title="各种并行化方案"></a>各种并行化方案</h3><p>不同的并行化方案，其实就是对应着怎么分配worker任务</p><ul><li>world_size 表示我们整个vllm里面启动了多少worker</li><li>tp_size 表示 TP启动的worker数量</li><li>pp_size 表示 PP启动的worker数量</li><li>pcp_size 表示PCP启动的worker数量</li><li>data_paraller_size 表示DP的worker数量</li></ul><blockquote><p><strong>Worker 本身必须具备同时参与多种并行策略组合的能力。</strong></p></blockquote><p>因为一个 Worker 不知道自己只属于 TP、PP 还是 DP，它只知道：</p><ul><li>自己的 global rank</li><li>自己属于哪个并行组</li><li>自己负责模型的哪一部分</li><li>自己需要和哪些 GPU 通信</li></ul><h4 id="TP，PP，DP，EP"><a href="#TP，PP，DP，EP" class="headerlink" title="TP，PP，DP，EP"></a>TP，PP，DP，EP</h4><p>先来解释一下这几个并行方案</p><p><img src="/images/Pasted%20image%2020260814182538.png"></p><p>vllm里面设计了一个5D张量来快速把一维的GPU rank list 转化成各个并行的peer分组。</p><p>但是我没有必要了解这个，你就理解，每个卡，他们的分工是什么就行了</p><blockquote><p>tp_size, pp_size, dp_size, ep_size, 这些都是说的你如何来划分对象</p><p>pp_size, 你把一个<mark style="background:#fff88f">模型副本来切分成pp_size段pipeline</mark><br>tp_size, 把每个pipeline, <mark style="background:#fff88f">里面的权重，切分成tp_size段</mark>（<mark style="background:#affad1">想想多头注意力，天然支持并行</mark>，不同语义空间维度互不相关，需要相关计算的时候，再每个tp组内peer，all-reduce一下）<br>dp_size, 这个最不同，他是描述你有几个模型副本<br>ep_size, 他是你<mark style="background:#fff88f">把所有的专家模型，分成几段</mark>，</p><p>上面这些你想象成任务，划分了各种任务栏<br>然后world_size 是表示你拥有的GPU，你现在要把这world_size个GPU分配到这些任务栏去。</p></blockquote><p>举例：tp_size &#x3D; 3, pp_size &#x3D; 5, 此时你现在有15个gpu</p><p>所以，可以这样分gpu的并行</p><p><img src="/images/Pasted%20image%2020260814183352.png" alt="512"></p><h4 id="PCP"><a href="#PCP" class="headerlink" title="PCP"></a>PCP</h4><blockquote><p>这个更加复杂，先把TP，PP搞懂。</p></blockquote><p>上下文并行（超长上下文）<br><img src="/images/Pasted%20image%2020260811153122.png" alt="278"></p><h3 id="物理拓扑-模型并行拓扑关系梳理"><a href="#物理拓扑-模型并行拓扑关系梳理" class="headerlink" title="物理拓扑-模型并行拓扑关系梳理"></a>物理拓扑-模型并行拓扑关系梳理</h3><p>假设：</p><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line"><span class="number">8</span> <span class="keyword">node</span> <span class="title">× 16</span> GPU</span><br><span class="line"><span class="attr">world_size=</span><span class="number">128</span></span><br></pre></td></tr></table></figure><p>遵循以下规则：</p><ul><li><strong>物理拓扑规则</strong>：<ul><li>world_size 永远表示整个vllm集群中用到的gpu数量</li><li>local_world_size 永远表示一个节点内的gpu的数量</li></ul></li><li><strong>模型并行拓扑规则</strong>：<ul><li><mark style="background:#affad1">1个dp副本 &#x3D; 1个enginecore引擎后端 &#x3D; 1个executor实例</mark>。</li><li>executor的类选择依赖于这个dp副本的gpu是否跨节点：<ul><li>不跨节点：<ul><li>一个节点里面跑多少个dp副本，就有多少个执行器，并且每个执行器都是MultiprocExecutor</li></ul></li><li>跨节点：<ul><li>一个dp副本一个enginecore进程一个executor，跑在多个节点上，这个进程，执行器肯定运行在一个节点上，所以是RayExecutor, </li><li>并且其余的节点上不会有enginecore, executor，仅仅只是被ray分布式框架创建好worker进程对象而已，被首节点的executor控制。</li></ul></li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260814191259.png"></p><table><thead><tr><th>配置</th><th>一个模型副本GPU</th><th>DP数量</th><th>每节点DP副本</th><th>EngineCore</th><th>Executor</th><th>Executor类型</th></tr></thead><tbody><tr><td>TP16 PP1 DP8</td><td>16</td><td>8</td><td>1</td><td>8</td><td>8</td><td>MultiprocExecutor</td></tr><tr><td>TP8 PP1 DP16</td><td>8</td><td>16</td><td>2</td><td>16</td><td>16</td><td>MultiprocExecutor</td></tr><tr><td>TP16 PP2 DP4</td><td>32</td><td>4</td><td>0.5</td><td>4</td><td>4</td><td>RayExecutor</td></tr><tr><td>TP16 PP8 DP1</td><td>128</td><td>1</td><td>0.125</td><td>1</td><td>1</td><td>RayExecutor</td></tr></tbody></table><h4 id="vllm-config"><a href="#vllm-config" class="headerlink" title="vllm_config"></a>vllm_config</h4><p><code>VllmConfig</code> 是整个推理实例的总配置，里面组合了多个子配置</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">VllmConfig</span>:</span><br><span class="line">    model_config <span class="comment"># 模型本身</span></span><br><span class="line">    <span class="comment"># 模型路径</span></span><br><span class="line">    <span class="comment"># 模型类型</span></span><br><span class="line">    <span class="comment"># dtype 参数类型</span></span><br><span class="line">    <span class="comment"># tokenizer</span></span><br><span class="line">    <span class="comment"># max_model_len</span></span><br><span class="line">    <span class="comment"># trust_remote_code</span></span><br><span class="line">    </span><br><span class="line">    cache_config <span class="comment"># kvcache相关</span></span><br><span class="line">    <span class="comment"># GPU cache占比</span></span><br><span class="line">    <span class="comment"># block_size = 16个token</span></span><br><span class="line">    <span class="comment"># KV cache dtype</span></span><br><span class="line">    <span class="comment"># swap space</span></span><br><span class="line">    </span><br><span class="line">    parallel_config <span class="comment">#并行化配置</span></span><br><span class="line">    <span class="comment"># tp,pp,dp</span></span><br><span class="line">    <span class="comment"># distributed_executor_backend = &quot;mp&quot;，“ray”,&quot;external_launcher&quot;</span></span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    scheduler_config <span class="comment"># 调度器配置</span></span><br><span class="line">    <span class="comment"># max_num_batched_tokens</span></span><br><span class="line">    <span class="comment"># max_num_seqs</span></span><br><span class="line">    <span class="comment"># scheduler policy</span></span><br><span class="line">    <span class="comment"># chunked prefill</span></span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    device_config <span class="comment"># 运行平台</span></span><br><span class="line">    <span class="comment"># device=&quot;cuda&quot;</span></span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    load_config <span class="comment"># 加载模型配置</span></span><br><span class="line">    <span class="comment"># safetensors</span></span><br><span class="line">    <span class="comment"># tensorizer</span></span><br><span class="line">    <span class="comment"># bitsandbytes</span></span><br><span class="line">    <span class="comment"># remote loading</span></span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    compilation_config <span class="comment"># 编译优化配置</span></span><br><span class="line">    <span class="comment"># torch.compile</span></span><br><span class="line">    <span class="comment"># CUDA graph</span></span><br><span class="line">    <span class="comment"># dynamo</span></span><br><span class="line">    </span><br><span class="line">speculative_config <span class="comment"># 投机解码配置</span></span><br><span class="line"><span class="comment"># draft model</span></span><br><span class="line"><span class="comment"># accept/reject</span></span><br></pre></td></tr></table></figure><p>让gpt列了一些实际的例子</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">VllmConfig</span>:</span><br><span class="line"></span><br><span class="line">    model_config              <span class="comment"># 模型本身配置</span></span><br><span class="line">        model = <span class="string">&quot;/models/Qwen2.5-7B-Instruct&quot;</span>              <span class="comment"># 模型路径</span></span><br><span class="line">        model_type = <span class="string">&quot;qwen2&quot;</span>                              <span class="comment"># 模型架构类型</span></span><br><span class="line">        tokenizer = <span class="string">&quot;/models/Qwen2.5-7B-Instruct&quot;</span>          <span class="comment"># tokenizer路径</span></span><br><span class="line">        max_model_len = <span class="number">32768</span>                              <span class="comment"># 最大上下文长度</span></span><br><span class="line">        dtype = <span class="string">&quot;float16&quot;</span>                                  <span class="comment"># 模型计算数据类型</span></span><br><span class="line">        seed = <span class="number">0</span>                                           <span class="comment"># 随机种子</span></span><br><span class="line">        trust_remote_code = <span class="literal">False</span>                          <span class="comment"># 是否允许加载远程自定义模型代码</span></span><br><span class="line">        limit_mm_per_prompt = <span class="literal">None</span>                         <span class="comment"># 单个请求最大多模态输入限制</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    cache_config              <span class="comment"># KV Cache相关配置</span></span><br><span class="line">        block_size = <span class="number">16</span>                                    <span class="comment"># 一个KV cache block包含的token数量</span></span><br><span class="line">        gpu_memory_utilization = <span class="number">0.9</span>                       <span class="comment"># GPU显存使用比例</span></span><br><span class="line">        swap_space = <span class="number">4</span>                                     <span class="comment"># CPU swap空间大小(GB)</span></span><br><span class="line">        cpu_offload_gb = <span class="number">0</span>                                 <span class="comment"># CPU offload显存大小</span></span><br><span class="line">        cache_dtype = <span class="string">&quot;auto&quot;</span>                               <span class="comment"># KV cache数据类型(fp16/bf16/fp8)</span></span><br><span class="line">        enable_prefix_caching = <span class="literal">True</span>                       <span class="comment"># 是否开启Prefix Cache复用历史KV</span></span><br><span class="line">        num_gpu_blocks = <span class="literal">None</span>                              <span class="comment"># 根据显存自动计算GPU KV block数量</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    parallel_config           <span class="comment"># 模型并行配置</span></span><br><span class="line">        tensor_parallel_size = <span class="number">4</span>                           <span class="comment"># Tensor Parallel大小，权重沿tensor维度切分的GPU数量</span></span><br><span class="line">        pipeline_parallel_size = <span class="number">1</span>                         <span class="comment"># Pipeline Parallel大小，模型层切分的stage数量</span></span><br><span class="line">        data_parallel_size = <span class="number">1</span>                             <span class="comment"># Data Parallel大小，模型副本数量</span></span><br><span class="line">        context_parallel_size = <span class="number">1</span>                          <span class="comment"># Context Parallel大小，沿sequence维度切分prefill</span></span><br><span class="line">        expert_parallel_size = <span class="number">1</span>                           <span class="comment"># MoE Expert Parallel大小，专家并行数量</span></span><br><span class="line">        distributed_executor_backend = <span class="string">&quot;mp&quot;</span>                <span class="comment"># Worker启动后端(mp/ray/external_launcher)</span></span><br><span class="line">        world_size = <span class="number">4</span>                                     <span class="comment"># 当前模型并行组总GPU数量(TP*PP*CP等)</span></span><br><span class="line">        rank = <span class="number">0</span>                                           <span class="comment"># 当前进程全局rank编号</span></span><br><span class="line">        local_rank = <span class="number">0</span>                                     <span class="comment"># 当前节点内rank编号</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    scheduler_config           <span class="comment"># 调度器配置</span></span><br><span class="line">        max_num_batched_tokens = <span class="number">8192</span>                      <span class="comment"># 单轮调度最大token数量</span></span><br><span class="line">        max_num_seqs = <span class="number">256</span>                                 <span class="comment"># 最大同时运行请求数量</span></span><br><span class="line">        policy = <span class="string">&quot;fcfs&quot;</span>                                   <span class="comment"># 调度策略(first come first serve)</span></span><br><span class="line">        enable_chunked_prefill = <span class="literal">True</span>                     <span class="comment"># 是否开启chunked prefill</span></span><br><span class="line">        max_num_partial_prefills = <span class="number">1</span>                      <span class="comment"># 最大同时进行部分prefill数量</span></span><br><span class="line">        enable_prefix_caching = <span class="literal">True</span>                      <span class="comment"># 是否允许调度prefix cache请求</span></span><br><span class="line">        enable_preemption = <span class="literal">True</span>                          <span class="comment"># 是否允许抢占低优先级请求</span></span><br><span class="line">        scheduling_steps = <span class="number">1</span>                              <span class="comment"># scheduler执行步数</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    device_config              <span class="comment"># 设备运行配置</span></span><br><span class="line">        device = <span class="string">&quot;cuda&quot;</span>                                    <span class="comment"># 推理设备类型(cuda/cpu)</span></span><br><span class="line">        device_id = <span class="number">0</span>                                      <span class="comment"># 当前GPU编号</span></span><br><span class="line">        platform = <span class="string">&quot;cuda&quot;</span>                                  <span class="comment"># 计算平台</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    load_config                <span class="comment"># 模型加载配置</span></span><br><span class="line">        load_format = <span class="string">&quot;auto&quot;</span>                               <span class="comment"># 权重加载格式(auto/safetensors/pt)</span></span><br><span class="line">        download_dir = <span class="string">&quot;~/.cache/huggingface&quot;</span>              <span class="comment"># 模型下载缓存目录</span></span><br><span class="line">        use_safetensors = <span class="literal">True</span>                             <span class="comment"># 是否优先加载safetensors格式权重</span></span><br><span class="line">        quantization = <span class="literal">None</span>                                <span class="comment"># 量化方式(None/AWQ/GPTQ/FP8)</span></span><br><span class="line">        load_sharded_state = <span class="literal">False</span>                         <span class="comment"># 是否分片加载模型权重</span></span><br><span class="line">        skip_loading_weights = <span class="literal">False</span>                       <span class="comment"># 是否跳过权重加载</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    compilation_config         <span class="comment"># 编译优化配置</span></span><br><span class="line">        enforce_eager = <span class="literal">False</span>                              <span class="comment"># 是否强制使用eager模式，关闭CUDA Graph</span></span><br><span class="line">        use_inductor = <span class="literal">False</span>                               <span class="comment"># 是否使用torch.compile + Inductor</span></span><br><span class="line">        backend = <span class="string">&quot;inductor&quot;</span>                               <span class="comment"># torch.compile后端</span></span><br><span class="line">        cuda_graph_capture_sizes = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">4</span>,<span class="number">8</span>,<span class="number">16</span>,<span class="number">32</span>,<span class="number">64</span>]      <span class="comment"># CUDA Graph捕获的batch size集合</span></span><br><span class="line">        max_capture_size = <span class="number">512</span>                             <span class="comment"># CUDA Graph最大capture batch大小</span></span><br><span class="line">        compile_mode = <span class="string">&quot;default&quot;</span>                           <span class="comment"># 编译优化模式</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    speculative_config          <span class="comment"># 投机解码配置</span></span><br><span class="line">        enabled = <span class="literal">False</span>                                  <span class="comment"># 是否开启投机解码</span></span><br><span class="line">        draft_model = <span class="string">&quot;/models/Qwen2.5-0.5B&quot;</span>              <span class="comment"># 草稿模型路径</span></span><br><span class="line">        num_speculative_tokens = <span class="number">5</span>                        <span class="comment"># 小模型一次预测token数量</span></span><br><span class="line">        acceptance_method = <span class="string">&quot;rejection_sampling&quot;</span>          <span class="comment"># 大模型验证接受策略</span></span><br><span class="line">        draft_tensor_parallel_size = <span class="number">1</span>                    <span class="comment"># 草稿模型TP大小</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    lora_config                 <span class="comment"># LoRA配置</span></span><br><span class="line">        enable_lora = <span class="literal">False</span>                               <span class="comment"># 是否开启LoRA</span></span><br><span class="line">        max_loras = <span class="number">4</span>                                     <span class="comment"># 最大同时加载LoRA数量</span></span><br><span class="line">        max_lora_rank = <span class="number">64</span>                                <span class="comment"># LoRA最大rank</span></span><br><span class="line">        lora_modules = []                                 <span class="comment"># LoRA模块列表</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    observability_config        <span class="comment"># 监控配置</span></span><br><span class="line">        enable_metrics = <span class="literal">True</span>                             <span class="comment"># 是否开启指标统计</span></span><br><span class="line">        collect_model_forward_time = <span class="literal">False</span>                <span class="comment"># 是否统计模型forward耗时</span></span><br><span class="line">        otlp_traces_endpoint = <span class="literal">None</span>                       <span class="comment"># OpenTelemetry trace地址</span></span><br></pre></td></tr></table></figure><h3 id="各种加速方案"><a href="#各种加速方案" class="headerlink" title="各种加速方案"></a>各种加速方案</h3><h4 id="torch-compile"><a href="#torch-compile" class="headerlink" title="torch.compile()"></a>torch.compile()</h4><p><img src="/images/Pasted%20image%2020260811154858.png"></p><p><strong>torch.compile：优化“计算图本身”。</strong></p><p>做的事情：</p><figure class="highlight delphi"><table><tr><td class="code"><pre><span class="line">PyTorch动态图</span><br><span class="line">    ↓</span><br><span class="line">捕获graph</span><br><span class="line">    ↓</span><br><span class="line">算子融合/生成优化kernel</span><br><span class="line">    ↓</span><br><span class="line">更快<span class="keyword">forward</span></span><br></pre></td></tr></table></figure><p>例如：原来：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">Linear kernel</span><br><span class="line"><span class="bullet">+</span></span><br><span class="line">Add kernel</span><br><span class="line"><span class="bullet">+</span></span><br><span class="line">ReLU kernel</span><br></pre></td></tr></table></figure><p>可能融合：</p><figure class="highlight"><table><tr><td class="code"><pre><span class="line">一个kernel完成</span><br></pre></td></tr></table></figure><h4 id="cuda-graphs"><a href="#cuda-graphs" class="headerlink" title="cuda graphs"></a>cuda graphs</h4><p><img src="/images/Pasted%20image%2020260811154728.png"><br><strong>CUDA Graph：优化“计算图执行过程”。</strong></p><p>不改变计算：第一次：</p><figure class="highlight avrasm"><table><tr><td class="code"><pre><span class="line"><span class="symbol">capture:</span></span><br><span class="line">kernel1</span><br><span class="line">kernel2</span><br><span class="line">kernel3</span><br></pre></td></tr></table></figure><p>之后：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">cudaGraphReplay</span><span class="params">()</span></span></span><br></pre></td></tr></table></figure><p>直接执行，不需要CPU逐个launch kernel。</p><p><img src="/images/Pasted%20image%2020260811155310.png"></p><h3 id="Worker"><a href="#Worker" class="headerlink" title="Worker"></a>Worker</h3><h4 id="worker的初始化"><a href="#worker的初始化" class="headerlink" title="worker的初始化"></a>worker的初始化</h4><p>主要是下面3件事情：</p><ul><li>init device(初始化设备)<ul><li>各种并行方案下，设备环境，分布式上下文的准备，多卡多节点的<mark style="background:#d3f8b6">通信组的准备</mark></li><li><mark style="background:#fff88f">model_runner实例化</mark>，管理调用各个算子来处理数据，采样器，GPU侧<mark style="background:#fff88f">输入输出缓冲区</mark>这些</li><li>inputbatch实例化，管理batch输入的状态（每个worker需要同时处理多个req）</li></ul></li><li>load model (加载模型)<ul><li>实例化各层<mark style="background:#fff88f">算子</mark></li><li>加载<mark style="background:#fff88f">模型权重</mark>，按照并行策略完成参数切分</li><li>切换pytorch的推理模式</li><li>torch.compile()对模型的执行图进行优化</li></ul></li><li>initialize <mark style="background:#fff88f">KV cache</mark>(初始化kv cache)<ul><li>确定各层的kvcache规格</li><li>估算可用显存，block数量</li><li>分配显存，绑定</li><li>flashattention后端配置</li><li>warmup批次，为常见batch_size的形状捕获cuda graphs, 减少kernel launch开销。</li></ul></li></ul><h3 id="executor"><a href="#executor" class="headerlink" title="executor"></a>executor</h3><ul><li>executor<ul><li>这个定位是：<mark style="background:#fff88f">一个 Executor 对应一个模型副本（model replica）的控制入口</mark></li><li>他不对应节点</li></ul></li><li>worker<ul><li>这个就是对应卡， </li><li><mark style="background:#fff88f">一个模型实例内部，可以有多个 Worker（因为 TP&#x2F;PP&#x2F;PCP）</mark></li></ul></li><li>节点：<ul><li><mark style="background:#fff88f">一个物理机器（服务器）通常对应一个 node（节点）</mark></li></ul></li></ul><p>所以，除了DP的情况下，其他任何一种情况，uni, mp, ray, 启用tp, pp, pcp优化，都只会有一个引擎后端进程，也就只有一个executor，也就是只有一个模型实例在跑。</p><p>如果在启用了DP的情况下，因为是直接把模型拷贝一份，并行跑，所以，会有多个引擎后端进程，就会有多个executor</p><p>为了加深DP和executor代表一个模型副本的理解，下面举两个例子</p><blockquote><p><strong>单机多卡</strong>：<br>单机8卡，DP&#x3D;2 TP&#x3D;4<br>含义：两个模型副本，每个副本用4卡跑，所以有两个executor，对应两个enginecoreproc, 运行在同一个机器上</p></blockquote><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">EngineCoreProc0</span><br><span class="line">      |</span><br><span class="line">   Executor0</span><br><span class="line">      |</span><br><span class="line">   Worker0-3</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">EngineCoreProc1</span><br><span class="line">      |</span><br><span class="line">   Executor1</span><br><span class="line">      |</span><br><span class="line">   Worker4-7</span><br></pre></td></tr></table></figure><blockquote><p>多机多卡：<br>2台机器，每台8卡，DP&#x3D;2 TP&#x3D;8<br>含义：一台机器一个节点，所以有两个节点，DP&#x3D;2， 所以一个节点跑一个副本，每个副本需要8张GPU</p></blockquote><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">Node0:(机0)</span><br><span class="line"></span><br><span class="line">Replica0（副本0 = DP0）</span><br><span class="line"></span><br><span class="line">EngineCoreProc0 </span><br><span class="line">      |</span><br><span class="line">  Executor0（一个副本对应一个executor）</span><br><span class="line">      |</span><br><span class="line"> Worker0-7</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Node1:（机1）</span><br><span class="line"></span><br><span class="line">Replica1（副本1 = DP1）</span><br><span class="line"></span><br><span class="line">EngineCoreProc1</span><br><span class="line">      |</span><br><span class="line">  Executor1（一个副本对应一个executor）</span><br><span class="line">      |</span><br><span class="line"> Worker8-15</span><br></pre></td></tr></table></figure><p><mark style="background:#ff4d4f">注意</mark><br>**这个 Executor 管理的 Worker<br>同一台机</p><hr><blockquote><p>但是你可以看到，这样的多进程架构是存在单机限制的，只有单机多卡的情况下，多进程才能互相通信，</p><p>但是如果分在多机，也就是多节点的情况下，这种多进程的架构就很难协同了，这就是<mark style="background:#fff88f">多机协同的和单机限制的矛盾</mark></p></blockquote><p>因此需要解决<mark style="background:#fff88f">跨机资源发现</mark>，<mark style="background:#fff88f">网络握手</mark>和<mark style="background:#fff88f">进程对齐</mark>的问题</p><p><img src="/images/Pasted%20image%2020260811201603.png" alt="518"></p><blockquote><p>Ray：<strong>分布式计算框架 &#x2F; 分布式任务调度框架</strong></p></blockquote><p>Ray主要解决：</p><blockquote><p>如何在多台机器上创建、管理、调度分布式计算任务。</p></blockquote><p>它提供：</p><ul><li>节点发现</li><li>进程&#x2F;对象管理</li><li>任务调度</li><li>Actor模型通信</li></ul><h4 id="通信网络"><a href="#通信网络" class="headerlink" title="通信网络"></a>通信网络</h4><h5 id="节点内部-GPU-GPU-通信"><a href="#节点内部-GPU-GPU-通信" class="headerlink" title="节点内部 GPU-GPU 通信"></a>节点内部 GPU-GPU 通信</h5><p><img src="/images/Pasted%20image%2020260811201956.png" alt="474"><br>既然提到了这个通信网络，这边就先梳理一下多机多卡的通信方式</p><p><img src="/images/Pasted%20image%2020260811202037.png" alt="313"></p><ul><li>单机内<ul><li>GPU-GPU通信：NVLink（最快），PCIe Switch（稍慢）</li></ul></li><li>多机间<ul><li>GPU（node 0）- GPU (node 1) :  RDMA+Ethernet&#x2F;InfiniBand<ul><li>依赖 CUDA + NCCL分布式通信库（先建立通信拓扑图）</li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260811202806.png" alt="338"></p><h5 id="节点之间的进程-任务管理和通信"><a href="#节点之间的进程-任务管理和通信" class="headerlink" title="节点之间的进程&#x2F;任务管理和通信"></a>节点之间的进程&#x2F;任务管理和通信</h5><p><img src="/images/Pasted%20image%2020260811202832.png" alt="202"></p><ul><li>ray<ul><li>负责：任务调度 + worker创建<ul><li>我提交RayExecutor()， ray发现各个节点的卡资源</li><li>然后ray告诉每个worker : 你的rank是多少 你的GPU是多少 master在哪里</li><li>worker启动后，由NCCL接管</li></ul></li></ul></li><li>slurm</li></ul><h4 id="并行组划分，5D张量表示"><a href="#并行组划分，5D张量表示" class="headerlink" title="并行组划分，5D张量表示"></a>并行组划分，5D张量表示</h4><p><img src="/images/Pasted%20image%2020260811204023.png" alt="452"></p><h3 id="executor和worker通信机制"><a href="#executor和worker通信机制" class="headerlink" title="executor和worker通信机制"></a>executor和worker通信机制</h3><p>这个也是进程间的通信机制</p><p>一种类 RPC 的进程间调用机制（vLLM 源码中正是将其核心方法命名为 <code>collective_rpc</code>）</p><blockquote><p>类似调用服务？把worker当成一个提供服务的进程</p></blockquote><p>RPC 是 <strong>Remote Procedure Call（远程过程调用）</strong>。</p><p>简单理解：</p><blockquote><p><strong>像调用本地函数一样，调用另一台机器&#x2F;另一个进程里的函数。</strong></p></blockquote><p>Executor 负责发送待执行的方法名及其参数，Worker 负责在自身进程中接收请求、执行对应逻辑，并将执行结果返回给 Executor。在这个抽象下，Executor 相当于调用方，Worker 相当于服务端。</p><h2 id="3-5-cuda-graph"><a href="#3-5-cuda-graph" class="headerlink" title="3.5_cuda_graph"></a>3.5_cuda_graph</h2><p><img src="/images/Pasted%20image%2020260822210611.png" alt="406"></p><p>A，B，C,….都表示cpu下发放到cuda stream里面的任务（异步）</p><p>即便如此，每次cpu下发任务，内核驱动解析，排队，调度，这个过程都会带来CPU的开销（多次进入内核态），尤其是在高频率的重复执行相同操作序列的场景下（推理中的每层的decode）这个时候的cpu开销（单进程，每层都要多次进入内核态提交）</p><p>所以cuda graph的设计就是消除这类重复性的调度成本：</p><p><strong>运行你在一次执行过程中，将一组已经定义好的，CUDA操作录制下来，包括kernel launch, 异步内存拷贝，事件同步等，记录下他们之间的依赖关系</strong></p><blockquote><p>这个录制的过程，就被成为<strong>图的捕获</strong></p></blockquote><p><strong>当这些操作被记录完成后，CUDA 运行时会将它们组织成一张有向无环图（DAG, Directed Acyclic Graph），这就是 CUDA Graph</strong></p><p>图中不仅包含有哪些操作，更重要的是还记录了操作之间的依赖关系。例如，某个 kernel 必须等前面的数据拷贝完成后才能启动，而另外一些彼此独立的操作则可以并行执行。正因为依赖关系被显式表达出来，系统在后续执行时就能够更准确地控制执行顺序。</p><p><mark style="background:#fff88f">更关键的是，一旦图被构建完成，后续重复执行时就不需要 CPU 再逐个调用 API、逐条提交操作、逐步建立依赖关系，而是只需要发起一次图执行请求。这样可以显著减少 CPU 的参与和提交开销</mark></p><p><strong>CUDA Graph 并不是简单地把多个 CUDA 操作打包在一起，而是把操作内容和依赖关系一起固化为可重复执行的执行计划</strong>。这样做的主要收益，是降低频繁重复执行相同计算流程时的调度成本，并让运行时基于完整的依赖信息更高效地组织执行</p><p><mark style="background:#d3f8b6">  更准确地说，CUDA Graph 的主要价值在于把一组固定的 CUDA 操作及其依赖关系提前固化下来，使后续执行时能够以更低的 CPU 提交成本完成同样的 GPU 工作。</mark></p><h3 id="使用注意事项"><a href="#使用注意事项" class="headerlink" title="使用注意事项"></a>使用注意事项</h3><p><img src="/images/Pasted%20image%2020260822211757.png"></p><ul><li>重放，依赖稳定的<strong>内存地址</strong></li></ul><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><p>在原有我们的基础上，做以下优化：</p><ol><li>提前申请静态张量，禁止动态形状<ol><li>这条执行路径上的 输入， 输出， 中间缓冲区， 都预先分配好。且形状必须保持一致</li></ol></li><li>录制期间，cpu不能同步，且纯cpu的行为无法被录制</li><li>cuda graph仅能记住内存地址，所以，需要实现原地写入 copy_() 、 fill_()<ol><li>xxx_表示in-place 原地修改数据<ol><li>fill_()表示，把整个Tensor的所有元素填成同一个值</li><li>copy_(), 把另一个tensor的数据原地写入当前张量</li></ol></li></ol></li></ol><h3 id="支持不同batch-size-不同序列长度"><a href="#支持不同batch-size-不同序列长度" class="headerlink" title="支持不同batch_size, 不同序列长度"></a>支持不同batch_size, 不同序列长度</h3><p>当我们面临不同batch形状的输入输出中间缓冲区的形状的需要的时候。</p><p>cuda Graph本身不擅长直接处理动态shape。所以有以下两种做法：</p><ol><li>为不同shape，或者不同bucket，分别录制多张graph，运行时按需选择合适的执行<ol><li>batch&#x3D;1, batch&#x3D;2</li><li>seq_len &#x3D; 512, 1024, 2048</li></ol></li><li>使用padding&#x2F;mask， 把较小输入映射到一张较大的固定shape上。</li></ol><h3 id="注意点"><a href="#注意点" class="headerlink" title="注意点"></a>注意点</h3><p>前面说到，我们cuda graph，依赖的是固定的地址，所以需要提前把输入输出，中间缓冲区创建出来，<br>这里的中间缓冲区，不是内部一个局部算子的输出的临时张量（这个可以直接录制地址）</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">Capture：</span><br><span class="line"></span><br><span class="line">graph_input</span><br><span class="line">   │ 0x1000</span><br><span class="line">   ↓</span><br><span class="line">Embedding</span><br><span class="line">   │</span><br><span class="line">   ├── 临时buffer 0x2000</span><br><span class="line">   ↓</span><br><span class="line">Layer 0</span><br><span class="line">   │</span><br><span class="line">   ├── 临时buffer 0x3000</span><br><span class="line">   ├── 临时buffer 0x4000</span><br><span class="line">   ↓</span><br><span class="line">Layer 1</span><br><span class="line">   │</span><br><span class="line">   ├── 临时buffer 0x5000</span><br><span class="line">   ...</span><br><span class="line">   ↓</span><br><span class="line">graph_output</span><br><span class="line">      0x9000</span><br><span class="line"></span><br><span class="line">              ↓</span><br><span class="line"></span><br><span class="line">这些 GPU 地址被 CUDA Graph 固定下来</span><br><span class="line"></span><br><span class="line">              ↓</span><br><span class="line"></span><br><span class="line">Replay：</span><br><span class="line"></span><br><span class="line">直接再次使用</span><br><span class="line">0x1000</span><br><span class="line">0x2000</span><br><span class="line">0x3000</span><br><span class="line">0x4000</span><br><span class="line">...</span><br><span class="line">0x9000</span><br></pre></td></tr></table></figure><p>这就是为什么你<strong>不用手工把模型 forward 里面所有中间 buffer 都提前创建出来</strong>。</p><p>PyTorch 在 capture 的时候就把这些中间显存分配纳入 graph 的内存管理了。</p><p><mark style="background:#d3f8b6">那什么时候需要把中间缓冲区提前创建出来呢？</mark></p><p><strong>需要提前创建中间缓冲区，通常不是因为 “CUDA Graph 一定要求你手动创建”，而是因为你想主动控制内存、shape、地址和生命周期</strong></p><p><img src="/images/Pasted%20image%2020260822220134.png" alt="506"></p><h3 id="为什么decode比prefill更适合-cuda-graph"><a href="#为什么decode比prefill更适合-cuda-graph" class="headerlink" title="为什么decode比prefill更适合 cuda graph"></a>为什么decode比prefill更适合 cuda graph</h3><p><mark style="background:#fff88f">decode</mark><br><img src="/images/Pasted%20image%2020260822220916.png" alt="154"></p><p><mark style="background:#fff88f">prefill</mark><br><img src="/images/Pasted%20image%2020260822221003.png" alt="268"></p><p><mark style="background:#fff88f">所以，在一个batch都是decode的req的时候，就是天然适合cuda graph的时候了</mark></p><p><img src="/images/Pasted%20image%2020260822221118.png" alt="356"><img src="/images/Pasted%20image%2020260822221132.png" alt="271"><br><img src="/images/Pasted%20image%2020260822221149.png" alt="247"></p><p>所以，如果是一个batch是混合的prefill&#x2F;decode， 这个就不是很方便</p><p>所以，一般在实际的推理框架中，<strong>只有纯 decode batch 才走 CUDA Graph，混入 prefill 就退回 eager</strong></p><p><img src="/images/Pasted%20image%2020260822221716.png" alt="524"></p><h3 id="vllm里的cuda-graph-实现"><a href="#vllm里的cuda-graph-实现" class="headerlink" title="vllm里的cuda_graph 实现"></a>vllm里的cuda_graph 实现</h3><p>关键数据结构：</p><ul><li>BatchDescriptor</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment"># 用来描述当前batch的关键形状特征</span></span><br><span class="line"><span class="comment"># 并作为CUDAGraph 分派、匹配使用的重要key</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BatchDescriptor</span>:</span><br><span class="line">num_tokens <span class="comment"># 该batch的总token数</span></span><br><span class="line">num_reqs <span class="comment"># 该batch的总req数</span></span><br><span class="line">uniform : <span class="built_in">bool</span> = <span class="literal">False</span> <span class="comment"># 表示当前batch中各个req的token数是否一致</span></span><br></pre></td></tr></table></figure><ul><li>CudaGraphEntry</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 用来保存某个BatchDescriptor 对应的 已捕获 CUDA graph， 以及该图重放时需要的相关信息</span></span><br><span class="line"><span class="comment"># 一个CUDAGraphEntry实例就对应一个图</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CUDAGraphEntry</span>:</span><br><span class="line">batch_descriptor: BatchDescriptor <span class="comment"># 该graph的输入batch的key</span></span><br><span class="line">cudagraph : torch.cuda.CUDAGraph <span class="comment"># 该graph本体</span></span><br><span class="line">output: <span class="type">Any</span> <span class="comment"># graph执行后的输出缓存，以弱引用形式保存，减少内存占用</span></span><br><span class="line">input_addresses: <span class="built_in">list</span>[<span class="built_in">int</span>] <span class="comment"># 输入张量的地址列表，用于在replay的时候校验输入地址是否一致</span></span><br></pre></td></tr></table></figure><p>model_runner实例，在init()初始化的时候，会创建cudagraphdispatcher调度器，但是此时的内部的key尚未真正初始化完成，因为他需要等待attention backend初始化结束后，才能确定当前后端实际支持那些cudagraph mode，以及应该生成那些可分派的BatchDescriptor</p><p>可以看一下cudagraphdispatcher内部的主要属性：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CudagraphDispatcher</span>:</span><br><span class="line">vllm_config <span class="comment"># 构造的配置</span></span><br><span class="line">uniform_decode_query_len = <span class="number">1</span>+ vllm_config.num_speculative_tokens <span class="comment"># 统一解码query长度</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.cudagraph_keys: <span class="built_in">dict</span>[CUDAGraphMode, <span class="built_in">set</span>[BatchDescriptor]] = &#123;</span><br><span class="line">CUDAGraphMode.PIECEWISE: <span class="built_in">set</span>(),</span><br><span class="line">CUDAGraphMode.FULL: <span class="built_in">set</span>(),</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.keys_initialized = <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 默认mode, 表示当前轮次的计算，该用什么策略，是分段图模式，还是完整图模式。</span></span><br><span class="line"><span class="variable language_">self</span>.cudagraph_mode = CUDAGraphMode.NONE</span><br></pre></td></tr></table></figure><hr><p>当构造一个空的CudagraphDispatcher后，model_runner会开始initialize_cudagraph_keys(), 为当前配置 和后端能力生成一组 有效的 cudagraph 调度key 然后放到调度器的容器里面</p><blockquote><p>所以这里你也看到了，调度器里面，只持有不同mode下的graph key, 并没有graph实例。<br>调度器，仅负责在容器里面，快速匹配合适的cudagraph描述符。</p></blockquote><p>后续的推理阶段，调度器，会根据输入的batch形状，返回</p><ul><li>runtime_mode<ul><li>用于描述本轮应采用的图类型，FULL &#x2F; PIECEWISE</li></ul></li><li>batch_descriptor (key)<ul><li>当前batch的关键形状特征，后续执行组件，再根据这两个信息，去查找是否已有捕获的 CUDA Graph可供重放</li></ul></li></ul><blockquote><p>所以，initialize_cudagraph_keys 所做的是：</p><ul><li>为mixed&#x2F;prefill 路径 预生成一批 PIECEWISE key</li><li>为 uniform decode 路径 预生成一批 FULL key</li><li>将这些key 按照 mode分类保存，供运行时分派使用</li></ul></blockquote><p><img src="/images/Pasted%20image%2020260823142850.png"></p><hr><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment"># 包装一个可执行对象，来增加 cuda图捕获和重放能力。</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">这个类的使用工作流：</span></span><br><span class="line"><span class="string">1. 初始化阶段，指定一个执行模式：FULL / PIECEWISE</span></span><br><span class="line"><span class="string">2. 运行阶段，wrapper实例，接受一个运行模式，一个batch key 从 前向上下文，然后相信这个调度结果</span></span><br><span class="line"><span class="string">3. 如果指定的运行模式是NONE， 就单纯的直接调用这个执行对象</span></span><br><span class="line"><span class="string">4. 否则，如果mode匹配上了wrapper的模式</span></span><br><span class="line"><span class="string">   4.1 如果key不存在，就执行图捕获，然后记录graph</span></span><br><span class="line"><span class="string">   4.2 如果key存在，就直接执行图重放</span></span><br><span class="line"><span class="string">   </span></span><br><span class="line"><span class="string">这个包装器，不会存吃持续缓存， 或者拷贝任何model的输入到重放的输入缓冲区。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CUDAGraphWrapper</span>:</span><br><span class="line">vllm_config</span><br><span class="line">compilation_config</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.runnable <span class="comment"># model</span></span><br><span class="line"><span class="variable language_">self</span>.runtime_mode <span class="comment">#支持的运行图模式, 不可以为NONE，否则不init</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 包装器里面，记录下我们目前已经捕获key-graph集合</span></span><br><span class="line"><span class="variable language_">self</span>.concrete_cudagraph_entries: <span class="built_in">dict</span>[BatchDescriptor, CUDAGraphEntry] = &#123;&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">self</span>.first_run_finished = <span class="literal">False</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以，当我们得到一个batch后，原本的逻辑，就是扁平化处理一下，然后塞给模型运行。</p><p>在这之中，我们增加了一个调度cuda graph的逻辑：</p><ul><li>输入batch</li><li>判断预处理：<ul><li><ol><li>batch类型：混合、纯decode、纯prefill</li></ol></li><li><ol start="2"><li>走什么mode: FULL完整图、PIECEWISE分段图</li></ol></li><li><ol start="3"><li>根据batch类型，判断是否要padding成固定形状</li></ol></li></ul></li></ul><h4 id="FULL-PIECEWISE-的理解"><a href="#FULL-PIECEWISE-的理解" class="headerlink" title="FULL PIECEWISE 的理解"></a>FULL PIECEWISE 的理解</h4><p>关键就在于：</p><ul><li>纯decode的batch， 只要padding到相同形状档位，他们的grid划分是可以复用的</li><li>非纯decode的batch， 即便padding到相同的形状档位，batch的形状不一样，导致grid的program的划分无法复用。</li></ul><p><img src="/images/Pasted%20image%2020260823171208.png"></p><h4 id="padding"><a href="#padding" class="headerlink" title="padding"></a>padding</h4><p><img src="/images/Pasted%20image%2020260823173528.png" alt="490"><br><img src="/images/Pasted%20image%2020260823173554.png" alt="468"><br><img src="/images/Pasted%20image%2020260823173500.png"></p><p>所以自始至终就是填的扁平input_ids， 这些属于填充，本质就是为了让graph能跑起来</p><p><strong>也没有掩码来排除填充，纯靠真实计数</strong></p><p><img src="/images/Pasted%20image%2020260823174030.png"></p><h4 id="实际流程"><a href="#实际流程" class="headerlink" title="实际流程"></a>实际流程</h4><p><img src="/images/Pasted%20image%2020260823214003.png" alt="412"></p><p><img src="/images/Pasted%20image%2020260823214754.png" alt="316"></p><h4 id="warmup"><a href="#warmup" class="headerlink" title="warmup"></a>warmup</h4><p>每次wrapper发现这次的（mode, key）还没有现成的graph, 那么在真正跑eager + capture之前，就会先跑几次dummy run, 目的是：</p><ul><li>加载&#x2F;编译 kernel <ul><li>triton kernel 第一次跑才编译，避免在capture的时候引入额外的launch</li></ul></li><li>稳定显存布局<ul><li>warmup用辅助stream，跑完sync同步，确保capture时显存池状态干净</li></ul></li></ul><blockquote><p><strong>cuda graph捕获时，任何新的kernel 编译&#x2F; 内存分配，都会被打进图里， warmup 先把这些 “一次性开销” 在 capture外触发掉，否则捕获到的图里面会包含 编译，分配等不该回放的东西。</strong></p></blockquote><h4 id="PIECEWISE的依赖"><a href="#PIECEWISE的依赖" class="headerlink" title="PIECEWISE的依赖"></a>PIECEWISE的依赖</h4><p>当trace到wrapper里面的调用的时候<br><img src="/images/Pasted%20image%2020260823233704.png" alt="416"><br>可以看到这边，如果是要录制的话，无论是FULL，还是PIECEWISE，都是一样的代码，所以wrapper只管录制操作，并没有区分。</p><p>所以PIECEWISE是分段图的录制，所以他不光光依赖CUDA graph， 还依赖torch.compiler</p><p><img src="/images/Pasted%20image%2020260823233835.png"></p><p>需要torch.compile 把模型的前向切开，否则cudagraph录制，永远只能录一段。</p><p>这就是为什么他们的简历里面只支持FULL的cuda graph</p><h4 id="流程展示"><a href="#流程展示" class="headerlink" title="流程展示"></a>流程展示</h4><p>【worker进程里model_runner的过程中】</p><ul><li><p>(构造) model_runner.init()</p><ul><li>配置元信息</li><li>构造采样器</li><li>kv cache tensor的列表创建</li><li>持久化请求状态字典</li><li>inputbatch 转换器</li><li><mark style="background:#fff88f">(保存) self.cudagraph_batch_sizes 保存排序后的 compilation_config.cudagraph_capture_sizes: list[int] 里面的图捕获大小列表</mark></li><li><mark style="background:#fff88f">(保存) self.uniform_decode_query_len 保存均匀解码(batch里都是decode req, 且qtoken一样多)</mark></li><li><mark style="background:#d3f8b6">（构造）self.cudagraph_dispatcher &#x3D; CudagraphDispatcher.init()</mark> 构造<mark style="background:#ff4d4f">graph调度器</mark></li><li>输入batch缓冲区创建</li></ul></li><li><p>(调用) model_runner.load_model()</p><ul><li>获取模型加载器，构造模型实例 + 权重拷贝</li><li><mark style="background:#fff88f">(保存) cudagraph_mode &#x3D; self.compilation_config.cudagraph_mode</mark><ul><li><mark style="background:#d3f8b6">发现确实是启动了cudagraph，mode不是NONE</mark>，是FULL_AND_PIECEWISE<ul><li><mark style="background:#affad1">(构造) self.model &#x3D; CUDAGraphWrapper()</mark> 构造<mark style="background:#ff4d4f">cudagraph模型装饰器</mark><ul><li><mark style="background:#b1ffff">self.concrete_cudagraph_entries 记录下我们目前已经捕获key-graph集合</mark></li></ul></li></ul></li></ul></li></ul></li><li><p>(调用) model_runner.profiler_run() 测算kvcache可用显存</p><ul><li>（调用）self. dummy_run()<ul><li>根据不同的情况，构造不同的dummy batch: 混合、纯decode、纯prefill</li><li><mark style="background:#fff88f">(调用) _determine_batch_execution_and_padding 计算该batch需要的mode, 以及padding</mark></li><li>创建dummy_run 计算这些dummy token所需的kvcache的槽位</li><li>前向推理<ul><li><mark style="background:#fff88f">设置 cuda graph 的 上下文</mark></li><li><mark style="background:#fff88f">调用self.model，驱动装饰器来执行模型，并录制模型的行为</mark></li></ul></li><li>返回结果</li></ul></li></ul></li><li><p>(调用) model_runner.initialize_kv_cache() 初始化kvcache显存为张量</p></li><li><h2 id="调用-model-runner-execute-model-更新inputbatch持久化信息-开始从inputbatch里面提取本次要执行的缓冲区信息，准备input-ids-（调用）-determine-batch-execution-and-padding-开始决定这个batch，他是走什么mode-什么形状，要不要padding。-读取已经计算好的slotmapping-整理格式-构建attention-元数据-统一预处理模型前向所需的全部输入-model-forward前向推理"><a href="#调用-model-runner-execute-model-更新inputbatch持久化信息-开始从inputbatch里面提取本次要执行的缓冲区信息，准备input-ids-（调用）-determine-batch-execution-and-padding-开始决定这个batch，他是走什么mode-什么形状，要不要padding。-读取已经计算好的slotmapping-整理格式-构建attention-元数据-统一预处理模型前向所需的全部输入-model-forward前向推理" class="headerlink" title="(调用) model_runner.execute_model()  - 更新inputbatch持久化信息  - 开始从inputbatch里面提取本次要执行的缓冲区信息，准备input_ids  - （调用） determine_batch_execution_and_padding 开始决定这个batch，他是走什么mode, 什么形状，要不要padding。  - 读取已经计算好的slotmapping, 整理格式  - 构建attention 元数据  - 统一预处理模型前向所需的全部输入  - model_forward前向推理"></a>(调用) model_runner.execute_model()<br>  - 更新inputbatch持久化信息<br>  - 开始从inputbatch里面提取本次要执行的缓冲区信息，准备input_ids<br>  - （调用） <em>determine_batch_execution_and_padding</em> 开始决定这个batch，他是走什么mode, 什么形状，要不要padding。<br>  - 读取已经计算好的slotmapping, 整理格式<br>  - 构建attention 元数据<br>  - 统一预处理模型前向所需的全部输入<br>  - model_forward前向推理</h2></li></ul><h2 id="4-并行优化策略"><a href="#4-并行优化策略" class="headerlink" title="4_并行优化策略"></a>4_并行优化策略</h2><h3 id="DP"><a href="#DP" class="headerlink" title="DP"></a>DP</h3><p>这边先展示一下DP，数据并行的一个架构图</p><p><img src="/images/Pasted%20image%2020260825145817.png"><br>可以看到，主要是增加了多个引擎后端（每个对应一个模型副本）<br>中间有一个协调器，</p><h4 id="wave的理解"><a href="#wave的理解" class="headerlink" title="wave的理解"></a>wave的理解</h4><p>wave这里就是引擎前端发过来的多个请求了，<strong>让多个模型副本，对当前处于第几轮协同执行保持一致，避免分布式场景下，由于请求到达时序不同造成的状态错位</strong>。</p><p><strong>在需要 wave 协调的场景下，多个<code>Engine Core</code> 虽然不一定处理完全相同的请求集合，但需要在同一个 wave 上协同推进</strong></p><p>可以将wave理解成一轮<strong>需要各个相关Engine core的模型副本，保持同步节奏</strong>的执行周期</p><blockquote><p>wave可以理解为是一次批量处理的周期，表示一次集体考试（每个dp模型副本在一个wave期间处理多个req）</p></blockquote><h4 id="前端-协调器-后端"><a href="#前端-协调器-后端" class="headerlink" title="前端-协调器-后端"></a>前端-协调器-后端</h4><h5 id="首次唤醒过程"><a href="#首次唤醒过程" class="headerlink" title="首次唤醒过程"></a>首次唤醒过程</h5><p><img src="/images/Pasted%20image%2020260825151153.png"></p><p>所以协调器是用来管理wave的</p><p>所以，wave数，进一步理解就是：</p><blockquote><p>wave数 就是所有 Engine 从运行状态 切换到暂停状态的次数。用于所有Engine的<br><strong>暂停-启动-运行-完成-暂停</strong><br>所以，每个引擎后端进程，接收到来自协调器中的 <mark style="background:#fff88f">START_DP_WAVE</mark>后，将 <mark style="background:#fff88f">engines_running</mark> 状态设置为True</p></blockquote><p>下面看一下，引擎后端进程，是如何被协调器的指令唤醒的<br><img src="/images/Pasted%20image%2020260825151712.png"></p><p><img src="/images/Pasted%20image%2020260825151739.png"></p><hr><p>除了上面的引擎前端-&gt;协调器， 发送唤醒启动指令，还有反向的通路</p><p>协调器 -&gt; 引擎前端</p><p>在这里，协调器向引擎前端发送后端引擎的负载情况。前端订阅这个地址，获取负载信息和request wave变化。</p><p><img src="/images/Pasted%20image%2020260825152049.png"></p><p>协调器向前端发送 三个信息：</p><ul><li>current_wave<ul><li>当前全局DP request wave编号。每当所有engines 从running 转为 paused 完成一轮后，该编号会推进，用于标记系统当前</li></ul></li><li>engine_req_consts_list<ul><li>各个引擎后端当前处理中的req负载信息，每个元素是一个(waiting, running)对，</li></ul></li><li>engines_running<ul><li>全局引擎后端的运行状态，为True表示当前的所有引擎后端整体处于running阶段。</li></ul></li></ul><h5 id="前端-协调器"><a href="#前端-协调器" class="headerlink" title="前端-协调器"></a>前端-协调器</h5><p><mark style="background:#d3f8b6">协调器</mark><br>设置了一个超时时间，每间隔一定时间 &#x2F; 有状态改变stats_changed, 就向引擎前端发送信息，来刷新引擎前端的3个状态</p><p><img src="/images/Pasted%20image%2020260825153110.png"></p><h5 id="协调器-引擎后端"><a href="#协调器-引擎后端" class="headerlink" title="协调器-引擎后端"></a>协调器-引擎后端</h5><blockquote><p>这里有一点需要注意： 请求的req推理，是不需要经过协调器的</p></blockquote><p><img src="/images/Pasted%20image%2020260825153305.png" alt="504"></p><p>下面来看一下，引擎前端发送给协调器，让他启动所有引擎后端的时机</p><ul><li><mark style="background:#fff88f">标准唤醒时机</mark><ul><li>引擎前端AsyncLLM 发送 <strong>FIRST_REQ</strong> , chosen_engine，给协调器，协调器接收到后，直接定向发送给这个后端进程 START_DP_WAVE</li></ul></li><li><mark style="background:#fff88f">异常情况处理时机</mark><ul><li>引擎后端在暂停状态下 收到过期wave的请求。就会上报给协调器，说发现一个过期的请求，但是我现在引擎关掉了，是否需要重启所有的engine来处理迟到的req</li></ul></li></ul><h5 id="engine间的同步"><a href="#engine间的同步" class="headerlink" title="engine间的同步"></a>engine间的同步</h5><p><img src="/images/Pasted%20image%2020260825153736.png"></p><p>在各个引擎后端进程中，他们处于run_busy_loop中，通过执行：<br><code>torch.distributed.all_reduce()</code><br><mark style="background:#fff88f">来同步全局是否本wave还有未完成的req状态</mark>， 目的是为了<strong>让不同的引擎后端进程执行的步调一致</strong>。</p><p>所以这个就是进程间通信（涉及节点内，节点间）</p><p><mark style="background:#fff88f">一旦所有 Engine 都执行完了各自的请求，它们同时进入暂停状态，wave 号递增，等待下一批 FIRST_REQ 启动</mark></p><p><img src="/images/Pasted%20image%2020260825154055.png"><br>所以这里的重点就是各个引擎后端进程的run_busy_loop中，需要通过进程间all reduce聚合，来同步整个全局引擎后端中，是否一起已经处理完了这一wave的所有req，都结束了，通知协调器，我们都做完了。</p><p><img src="/images/Pasted%20image%2020260825155158.png"></p><p>下面有几个零碎的概念点：</p><ol><li>内部负载均衡模式</li></ol><p>就是协调器接受了所有引擎后端的负载状态后，会转发给前端。如果我们启用了内部负载均衡模式，意思就是<mark style="background:#fff88f">前端根据这个负载情况，计算分数</mark>。然后选择分数最低的Engine, 选中后，前端还会临时增加本地的等待计数waiting count, 以缓解统计更新间隔带来的请求倾斜。</p><ol start="2"><li>关于DPEngineCoreProc和EngineCoreProc的使用区别</li></ol><p><img src="/images/Pasted%20image%2020260825155710.png"></p><p>总结一下，相比于普通的无并行的引擎后端，DP多了</p><ul><li>每个引擎后端，按需上报负载</li><li>判断本地未完成请求，all-reduce 判断全局未完成请求</li><li>wave结束后，dp_rank为0的引擎后端，负责发送wave_complete 给协调器</li></ul><h4 id="同步场景的处理"><a href="#同步场景的处理" class="headerlink" title="同步场景的处理"></a>同步场景的处理</h4><p>涉及3个方面：</p><ul><li>引擎前端<ul><li>发生请求时，携带自己当前记录的 current_wave</li></ul></li><li>协调器</li><li>引擎后端<ul><li>收到请求后，将request_wave与本地的current_wave进行比较</li></ul></li></ul><p>所以存在两类同步场景：</p><ul><li>正常同步<ul><li>当前wave结束，Engine通知协调器，推进wave</li><li>前面主要描述的是这个情况</li></ul></li><li>补偿同步<ul><li>请求携带的wave 和 Engine当前的wave不一致，需要通过协调器补发启动信号，使其他Engine跟上</li></ul></li></ul><p>在系统运行过程中，可能存在一种典型但复杂的情况：前端（AsyncLLM）发出请求的速度与 Engine 的状态推进存在不一致，从而引入潜在的时序冲突。</p><p><img src="/images/Pasted%20image%2020260825160630.png"><br><img src="/images/Pasted%20image%2020260825161022.png"></p><h4 id="DP如何部署？"><a href="#DP如何部署？" class="headerlink" title="DP如何部署？"></a>DP如何部署？</h4><h5 id="单机多卡"><a href="#单机多卡" class="headerlink" title="单机多卡"></a>单机多卡</h5><p><img src="/images/Pasted%20image%2020260825161335.png"></p><p>单机多卡下：<br>每个引擎后端进程，表示一个模型副本，data_parallel_size表示模型副本数，<mark style="background:#fff88f">分别处理不同子集的输入请求</mark></p><p>请求分配方面，vLLM 将<strong>输入批次切分为若干部分</strong>，逐一交给各个进程（或 GPU）处理；通过 NVIDIA NCCL 库广播或分发输入，确保每个 GPU 得到正确的请求子集</p><h5 id="DP-4，TP-2"><a href="#DP-4，TP-2" class="headerlink" title="DP&#x3D;4，TP&#x3D;2"></a>DP&#x3D;4，TP&#x3D;2</h5><p>说明有4个模型副本，每个副本，由2张GPU的卡完成</p><p><img src="/images/Pasted%20image%2020260825161711.png"></p><h5 id="多机多卡，分布式场景下的数据并行"><a href="#多机多卡，分布式场景下的数据并行" class="headerlink" title="多机多卡，分布式场景下的数据并行"></a>多机多卡，分布式场景下的数据并行</h5><p><img src="/images/Pasted%20image%2020260825161903.png"></p><p>这里的情况就是，</p><ul><li>节点0<ul><li>告知，我们DP&#x3D;4，一共有4个模型副本，然后本地节点有2个副本（说明还有2个副本在其他节点）（0，1副本），然后这个0节点作为头结点。并且指定这个头结点的通信URL</li></ul></li><li>节点1<ul><li>告知，我们是DP&#x3D;4，这个节点上运行两个副本，且开始的dp分支rank是2开头（2，3），并指定我们这个节点是无头模式，说明是从别的节点的前端进程和协调器来接受指令。并指明<mark style="background:#fff88f">DP master 节点的 IP 地址</mark></li></ul></li></ul><h5 id="多机多卡下的两层通信"><a href="#多机多卡下的两层通信" class="headerlink" title="多机多卡下的两层通信"></a>多机多卡下的两层通信</h5><ul><li>控制面通信<ul><li>data-parallel-address, data-parallel-rpc-port</li><li>负责DP rank分配，EngineCore的注册，请求的路由，状态同步</li><li>这里主要是用于RPC通信的</li><li><img src="/images/Pasted%20image%2020260825162525.png" alt="108"></li></ul></li><li>GPU计算通信<ul><li>TP中的，GPU0-NCCL-GPU1</li><li>PP中的</li><li>这里主要涉及的是：NCCL，RDMA，NVLink, PCIe</li></ul></li></ul><h5 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h5><p><strong>DP 推理服务流程总结：多进程加载模型副本 -&gt; 划分请求 -&gt; 并行生成 -&gt; 汇总结果</strong></p><h5 id="TP-DP"><a href="#TP-DP" class="headerlink" title="TP+DP"></a>TP+DP</h5><p>张量并行 + 数据并行， 两者是正交的，可以同时来用</p><p>下面看一个TP +DP的例子</p><p>TP&#x3D;8, DP&#x3D;64<br>表示，模型副本64个，每个副本由8张卡一起跑。所以一共启动了512个GPU</p><blockquote><p>张量并行：<br>同一个TP组内的多个卡，共同承载一份模型副本，vllm称为 TP组，组内通信包括all-reduce, all-gather, reduce-scatter 等。具体取决于算子实现（列并行，行并行，vocab并行Embedding，并不是每层仅有all-reduce）。</p></blockquote><blockquote><p>数据并行：<br>同一个TP组内处于相同TP位置的GPU形成我们的DP组，这些rank各自持有参数相同的完整模型副本。在vllm中，DP组之间的all-reduce, 主要用于调度协同(全局未完成请求，Wave状态等)。而且，MOE与dense的模型，DP的行为也不同</p></blockquote><blockquote><p>通信底层：<br>通过pytorch的 <code>torch.distributed</code> 发起，默认是NCCL backend, 同一个节点的TP还可能走custom all-reduce等优化路径</p></blockquote><p>所以下面展示一下<strong>DP&#x3D;64 + TP &#x3D;8</strong>的一个工作情况 + Moe在DP时候也要涉及all-reduce</p><p><img src="/images/Pasted%20image%2020260825163720.png"></p><p><img src="/images/Pasted%20image%2020260825163931.png"><br><img src="/images/Pasted%20image%2020260825164157.png"></p><p><img src="/images/Pasted%20image%2020260825164239.png"></p><h5 id="启动流程分析"><a href="#启动流程分析" class="headerlink" title="启动流程分析"></a>启动流程分析</h5><h3 id="TP，PP"><a href="#TP，PP" class="headerlink" title="TP，PP"></a>TP，PP</h3><p>下面来开始正式进行需要计算通信的两个并行方案：</p><ul><li>张量并行</li><li>流水线并行</li></ul><p>他们的核心思路，就是把单个模型部分，拆分到多个GPU共同完成计算。这样就可以降低单张GPU的显存压力：</p><blockquote><p>prefill是计算密集型，decode是访存密集型</p></blockquote><p><mark style="background:#fff88f">当我们的计算量足够大，因此通信开销就可以被摊薄了，从而提升forward吞吐</mark>。</p><p>在vllm中，借鉴了Megatron风格的TP， 常见的切分位置有3类：</p><ul><li><strong>Embedding层</strong>（这个就是个大权重矩阵，查表）<ul><li><mark style="background:#d3f8b6">VocabParallelEmbedding层，就是按照vocab维度，切分embedding权重，每张卡只负责一部分的token， forward之后，通过all-reduce 汇总完整的embedding</mark></li></ul></li><li>线性层（就是个巨大的Matmul）<ul><li>MergedColumnParallelLinear, RowParallelLinear，QKVParallelLinear这些层定义</li><li><mark style="background:#d3f8b6">Column Parallel 切输出维度</mark></li><li><mark style="background:#d3f8b6">Row Parallel 切输入维度</mark></li></ul></li><li>Attention<ul><li>Q,K,V向量，按照head维，分到不同的GPU上，每个卡，<mark style="background:#d3f8b6">负责计算自己负责的几个头的attention</mark>，最后的O通过<mark style="background:#d3f8b6">Row Parallel 和 All-Reduce 合并结果</mark>。</li></ul></li></ul><p>后面我们就按 <code>VocabParallelEmbedding</code>、<code>Column Parallel</code>、<code>Row Parallel</code> 和 <code>LM Head</code> 这几类层，分别看它们在 forward 中什么时候触发 <strong><code>All-Reduce</code></strong>、<strong><code>All-Gather</code></strong> 或 <strong>gather</strong>。</p><h4 id="AllReduce原理"><a href="#AllReduce原理" class="headerlink" title="AllReduce原理"></a>AllReduce原理</h4><p><mark style="background:#fff88f">All-Reduce的目标，是将所有进程上的数据，通过特定的操作（求和，取max）聚合后，把结果同步到每一个进程。</mark></p><p>所以，<strong>把All-Reduce看成一个方法</strong></p><p>常见的底层实现包括：</p><ul><li>Ring AllReduce</li><li>Tree AllReduce</li></ul><p><mark style="background:#fff88f">其最终目标，就是让每块 GPU 上的数据都变成汇总&#x2F;归约后的同一个结果</mark></p><h5 id="Ring-AllReduce-算子原理"><a href="#Ring-AllReduce-算子原理" class="headerlink" title="Ring-AllReduce 算子原理"></a>Ring-AllReduce 算子原理</h5><p>Ring-AllReduce 的 实现，其实分为两个过程 <strong>Reduce-Scatter</strong> 和 <strong>All-Gather</strong></p><ol><li><strong>分块传输</strong><br> 假设有N个进程，每个进程有一份数据（待规约）。Ring-AllReduce 会先把这份数据 切分成N个chunk。<br><br> 然后，把这些进程在逻辑上，组成一个环。</li><li><strong>Reduce-Scatter阶段</strong><br> 在每一轮中，每个进程，都会把一个chunk发给环上的下一个进程。同时从上一个进程接受一个chunk。<br><br> 收到chunk后，进程会把它和本地对应位置的chunk 做 逐元素规约（求和）。经过N-1轮后，每个进程，就会得到一块已完成全局规约的结果分片。此时每个进程只保存完整结果的一部分。</li></ol><p>这里简单说，就是，N个进程，每个进程i，在N-1次循环中，将上一次接受到的j位置的数据发给下一个进程的同样位置，然后接受到前面一个进程的他的位置的数据，然后加在自己的位置上。</p><p>这样就是类似，N个进程，每次都分配到不同的位置进行加法计算。</p><p><img src="/images/Pasted%20image%2020260825173506.png" alt="410"></p><p><img src="/images/Pasted%20image%2020260825173516.png" alt="422"><br><img src="/images/Pasted%20image%2020260825173527.png" alt="405"><br><img src="/images/Pasted%20image%2020260825173540.png" alt="400"></p><p>最终，分片规约，导致，N个进程，每个都拥有独立的一个chunk，里面已经Ring规约好了全局的结果。</p><p>所以下面的需求就是要把这些chunk，通知到所有人（N个进程）</p><ol start="3"><li><strong>All-Gather阶段</strong><br> 各个进程，继续沿着环传播这些已经规约完成的结果分片。每一轮发送当前持有的结果分片，同时接受其他进程传来的结果分片。<br><br> 经过N-1轮后， 所有进程都能收集到全部规约分片，并把这些分片拼接成完整的AllReduce结果。</li></ol><p>依然遵循上面的相邻GPU的对应位置互换通信原则。只是这一轮不再做相加。而是将已经规约好的分块拷贝到下一跳的对应位置上。</p><p><img src="/images/Pasted%20image%2020260825174000.png"></p><h5 id="Ring-AllReduce的通信成本"><a href="#Ring-AllReduce的通信成本" class="headerlink" title="Ring-AllReduce的通信成本"></a>Ring-AllReduce的通信成本</h5><ul><li>N个进程</li><li>每个进程K份chunk数据</li></ul><p>一次AllReduce中</p><ul><li>N-1次reduce-scatter<ul><li>每次ring, 传递K&#x2F;N个数据</li></ul></li><li>N-1次Allgather<ul><li>每次ring, 传递K&#x2F;N个数据</li></ul></li></ul><p>所以，整个AllReduce的传输数据大小为$$2(N-1) * K&#x2F;N$$<br><strong>随着</strong>N <strong>的增大，Ring AllReduce 通信算子的通信量可以近似为</strong> $$2K$$<img src="/images/Pasted%20image%2020260825174720.png"></p><p>Ring All-Reduce的吞吐，每次传输的数据量是K&#x2F;N, 当N增大的时候，一次Ring的数据就变少了，所以对于K很大，就容易形成流水线并行，通信效率很高(每次传输K&#x2F;N个数据，N-1次完成，效率很高)</p><p>但是对于小张量的情况，固定调度开销和协议开销的占比会明显上升，导致实际的带宽利用率下降。</p><p>另外，Ring All-Reduce需要完成2(N-1)次通信，所以如果小张量的话，多轮通信次数不变，所以多轮通信带来的延迟累计会成为瓶颈</p><p>因此，<code>Ring AllReduce</code> 一般不适合小张量频繁同步、且对延迟要求很高的场景<br>（就是规约并行节点特别多，每个节点的数据又不算多。）</p><p><img src="/images/Pasted%20image%2020260825175426.png"></p><p><mark style="background:#fff88f">vllm里面使用的低延时 All-Reduce的核心思想</mark>，是:</p><p><mark style="background:#fff88f">各GPU通过预先建立的通信资源，访问其他rank的数据，并完成逐元素求和，似的每个GPU最终得到与AllReduce(sum)等价的全局规约结果（就是直接K&#x3D;1）</mark><br><img src="/images/Pasted%20image%2020260825175900.png" alt="562"></p><p>从逻辑效果上看，每个 GPU 都会执行一次等价的规约过程，并得到与 <code>AllReduce(sum)</code> 相同的输出。和 <code>Ring AllReduce</code> 相比，这类低延迟路径<mark style="background:#fff88f">不强调多轮分块传递</mark>，而是<mark style="background:#d3f8b6">利用预先建立的通信资源</mark>、<mark style="background:#d3f8b6">共享缓冲区等资源</mark>，<strong>尽量减少通信轮次和 kernel 调度次数</strong>，<strong>从而降低小张量频繁同步时的总延迟</strong>。</p><h3 id="Transformer的TP"><a href="#Transformer的TP" class="headerlink" title="Transformer的TP"></a>Transformer的TP</h3><h4 id="输入维度，输出维度"><a href="#输入维度，输出维度" class="headerlink" title="输入维度，输出维度"></a>输入维度，输出维度</h4><p>首先要建立一个规范，假设一个GEMV的向量乘矩阵：<br>X（1，4）x   W (4, 2) &#x3D; O（1，2）</p><p>所以输入向量X，输入维度&#x3D;input_size &#x3D; 4</p><p>所以按照这种写法的W的输入维度就是4（作为axis0第一个维度）， 输出维度就是2（axis1第二个维度）。</p><p><strong>我们后面说的列并行，行并行，都是基于这个矩阵形式来理解的</strong>。</p><h4 id="矩阵的列并行，行并行"><a href="#矩阵的列并行，行并行" class="headerlink" title="矩阵的列并行，行并行"></a>矩阵的列并行，行并行</h4><p>所谓TP，张量并行（模型并行），本质就是把原本交给Matmul算子做并行的工作，由于算力的原因，先行一步，在输入算子前，就先行拆分到不同的gpu上去。本质还是分块矩阵的应用</p><p>下面看一下在定义好了矩阵的输入，输出维度后，列并行和行并行的区别。</p><p><img src="/images/Pasted%20image%2020260825211842.png"><br>可以看到：</p><ul><li><mark style="background:#d3f8b6">列并行</mark><ul><li>就是切分输出维度+拼接</li></ul></li><li><mark style="background:#d3f8b6">行并行</mark><ul><li>就是切分输入维度+逐元素求和</li></ul></li></ul><p>图示为：</p><p><img src="/images/Pasted%20image%2020260826201005.png"></p><p>上面展示了并行计算的matmul，所以这种张量并行，会出现在MLP，MHA，Embedding层的算子中</p><h5 id="MLP层的张量并行"><a href="#MLP层的张量并行" class="headerlink" title="MLP层的张量并行"></a>MLP层的张量并行</h5><p>一层MLP主要有两个部分，矩阵乘GEMM + 激活函数GeLU(SiLU)</p><p><img src="/images/Pasted%20image%2020260826202030.png" alt="321"></p><p>下面展示了列并行， 行并行的各自的优点<br><img src="/images/Pasted%20image%2020260826202859.png"></p><p><img src="/images/Pasted%20image%2020260826202954.png"></p><p><img src="/images/Pasted%20image%2020260826203003.png"></p><p>这里就有一个使用时的连续技巧：<br>先列切分，再行切分，可以直接自然的接上 </p><p><img src="/images/Pasted%20image%2020260826203332.png" alt="411"></p><p><img src="/images/Pasted%20image%2020260826203344.png"></p><p><mark style="background:#affad1">因此，transformer块中的MLP层的TP并行拆分步骤，可以简单总结为</mark>：<br><img src="/images/Pasted%20image%2020260826203634.png"></p><h6 id="带有SwiGLU的MLP"><a href="#带有SwiGLU的MLP" class="headerlink" title="带有SwiGLU的MLP"></a>带有SwiGLU的MLP</h6><p>我们真实的大模型里面的FNN，不是就单纯的两个线性层，而是带有SwiGLU的门控的，所以先复习一下门控相关的知识</p><p><img src="/images/Pasted%20image%2020260826210230.png"></p><p>所以，我们的目的是每个GPU分别持有每个权重矩阵的一部分，然后利用先列后行来减少GPU之间的通信。</p><p>所以下面展示一下，具体是如何切分，并行的。</p><blockquote><p>由于MLP层是对batch的形状不敏感的，所以是逐token向量计算</p></blockquote><ul><li>假设输入token 是 h维度的</li><li>up后的高维度是i</li></ul><p><img src="/images/Pasted%20image%2020260826213802.png"></p><p>可以看到，<mark style="background:#fff88f">由于SwiGLU的SiuAndMul，由于是对应元素乘，所以在列并行的情况下可以直接本地做</mark><br>而我们的原始的如果一个GPU持有Wgate, 一个GPU持有Wup，则需要一次all-reduce通信。</p><p>非常的巧妙。</p><p>所以，<mark style="background:#fff88f">ColumnParallelLinear</mark>，就是指的这个层算子底层持有的张量是列并行切分后的张量<br><mark style="background:#fff88f">MergedColumnParallelLinear</mark> 就是指的他持有的是Wgate,Wup两个部分的切分张量<br><mark style="background:#fff88f">RowParallelLinear</mark>层算子底层持有的张量是行并行切分后的张量。</p><p>通过参数：</p><ul><li>gather_output&#x3D;False<ul><li>表示输出的结果无需all-reduce</li></ul></li><li>input_is_parallel&#x3D;True<ul><li>表示输入是并行输入，是接的别的并行输出。</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260826214651.png" alt="514"></p><p>所以每个层的前向，定义的都是一个GPU的行为。</p><p>关于推理的时候，TP并行数的规约的通信量的计算</p><p><img src="/images/Pasted%20image%2020260826214857.png"></p><h5 id="MHA层的张量并行"><a href="#MHA层的张量并行" class="headerlink" title="MHA层的张量并行"></a>MHA层的张量并行</h5><p>这个就更简单了，因为TP是在权重张量上的并行划分，所以，MHA层里面，受到TP影响的只有QKV投影矩阵，以及O输出矩阵，每个GPU各持有QKVO的一部分权重，分别计算各自的头，然后每个GPU上的flashattention分别计算负责的头的注意力输出即可。</p><p><img src="/images/Pasted%20image%2020260826231503.png"></p><p><img src="/images/Pasted%20image%2020260826231646.png"></p><p>每个头在每个GPU上是独立计算的，所以，就是把每个头的参数，放到一块GPU上，最后将子结果concat后，得到最终的张量</p><p>当然，实际使用中，一个GPU肯定会负责多个头，<mark style="background:#fff88f">所以我们要尽量保证heads总数能够被GPU个数整除。</mark></p><p>当我们每张卡，计算完各自的头的注意力输出头后，我们就可以很自然的进行 行并行O的切分。</p><p><img src="/images/Pasted%20image%2020260826232137.png"></p><p><img src="/images/Pasted%20image%2020260826232155.png"></p><p><img src="/images/Pasted%20image%2020260826232223.png"></p><h5 id="Embedding层的张量并行"><a href="#Embedding层的张量并行" class="headerlink" title="Embedding层的张量并行"></a>Embedding层的张量并行</h5><p>LM head的权重张量是(vocab_size, hidden_size)<br><img src="/images/Pasted%20image%2020260826233006.png" alt="235"></p><p>因为这个张量权重，vocab_size是很大的。所以，一般可以在这个输入维度上做<mark style="background:#fff88f">行切分</mark>，就可以减少单张GPU上的计算量，显存占用</p><p><mark style="background:#fff88f">在vllm中，输入Embedding和输出lm head， 都是采用按照词表维度切分的方式。但是两者的通信方式不一样</mark>。</p><ul><li>Embedding, 在各个GPU内得到局部结果，通过all-reduce把结果相加（虽然都是0）<ul><li><img src="/images/Pasted%20image%2020260827090509.png" alt="338"></li></ul></li><li>lm head, 每个GPU先计算各自负责的vocab词表分片的logits， 然后在logits处理阶段执行all-gather， 得到完整vocab的logits（因为要统计所有的vocab的打分，才能输出一个公平的全局打分）<ul><li><img src="/images/Pasted%20image%2020260827091145.png" alt="477"></li></ul></li></ul><h4 id="vllm中的张量并行"><a href="#vllm中的张量并行" class="headerlink" title="vllm中的张量并行"></a>vllm中的张量并行</h4><p>先来理解一下分布式通信的一个概念：</p><ul><li>All-Reduce:<ul><li>每张卡都有一个同形状张量，各卡对应位置做规约，通常是<mark style="background:#fff88f">求和</mark></li></ul></li><li>All-Gather:<ul><li>每张卡持有结果的一部分，<mark style="background:#fff88f">concat拼接</mark></li></ul></li></ul><h5 id="vllm的并行线性层"><a href="#vllm的并行线性层" class="headerlink" title="vllm的并行线性层"></a>vllm的并行线性层</h5><h6 id="ColumnParallelLinear"><a href="#ColumnParallelLinear" class="headerlink" title="ColumnParallelLinear"></a>ColumnParallelLinear</h6><ul><li>LinearBase 基类<ul><li>ColumnParallelLinear 列并行子类，包装实现列并行，每个子类持有的权重仅为列维度的某一块。</li></ul></li></ul><h6 id="RowParallelLinear"><a href="#RowParallelLinear" class="headerlink" title="RowParallelLinear"></a>RowParallelLinear</h6><ul><li>LInearBase 基类<ul><li>RowParallelLinear 行并行子类，包装实现行并行，每个子类持有的权重仅为输入维度（行维度）的一部分，所以输入就是一个并行切片输入<ul><li>一般输出默认会all-reduce 求和</li></ul></li></ul></li></ul><h6 id="QKVParallelLinear"><a href="#QKVParallelLinear" class="headerlink" title="QKVParallelLinear"></a>QKVParallelLinear</h6><ul><li>ColumnParallelLinear 列并行类，表示这个类持有一部分列维度<ul><li>QKVParallelLinear 子类，<ul><li>将原本的W_q, W_k, W_v 3个线性层，合并成一个W_qkv 矩阵，沿着输出维度拼接。</li><li><strong>朴素实现中</strong>，这一个子类持有W_q,W_k,W_v的全部权重。一个hidden_state， 进行一次线性计算，就可以得到q_k_v的一个大的拼接向量。后续模型按照这个维度，切分层qkv，分别输入到不同的地方。</li><li>但是在<strong>张量并行下</strong>，这个子类，持有的是W_q,W_k,W_v的列切片，所以算出来的q_k_v仅是部分qkv头的concat</li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260827095856.png"></p><h5 id="vllm里面的流水线并行"><a href="#vllm里面的流水线并行" class="headerlink" title="vllm里面的流水线并行"></a>vllm里面的流水线并行</h5><p><strong>关于PP中的bubble问题</strong></p><p><img src="/images/Pasted%20image%2020260827102540.png"></p><p><img src="/images/Pasted%20image%2020260827102609.png" alt="567"></p><p><img src="/images/Pasted%20image%2020260827102627.png"></p><p><img src="/images/Pasted%20image%2020260827102652.png"><br><img src="/images/Pasted%20image%2020260827102714.png"></p><p><mark style="background:#fff88f">解决办法就是</mark></p><p><img src="/images/Pasted%20image%2020260827102742.png"><br><img src="/images/Pasted%20image%2020260827102802.png"><br><img src="/images/Pasted%20image%2020260827102819.png"></p><h5 id="vllm里面这一块的实现梳理"><a href="#vllm里面这一块的实现梳理" class="headerlink" title="vllm里面这一块的实现梳理"></a>vllm里面这一块的实现梳理</h5><p>因为这边遇到了很多不同优化方案的交回，所以，我这里先做一些概念的梳理</p><h6 id="1-异步调度，采样token留在gpu，异步回传的逻辑"><a href="#1-异步调度，采样token留在gpu，异步回传的逻辑" class="headerlink" title="1. 异步调度，采样token留在gpu，异步回传的逻辑"></a>1. 异步调度，采样token留在gpu，异步回传的逻辑</h6><p>这里要区分：异步、同步调度，GPU-CPU拷贝，流式输出 这几个概念的关联</p><p>输入通道：<br>同步调度，异步调度<br>是说，你每次发过来的batch，是否需要execute_model阻塞等待worker返回，是enginecore的行为</p><p>GPU-CPU拷贝（同步拷贝，异步拷贝）<br>是说这个GPU内采样出来的token id，拷贝到CPU，单纯指拷贝D2H，H2D的拷贝开销，这里分同步拷贝和异步拷贝。是worker进程的行为</p><p>流式输出<br>这个仅仅只是输出链路的要求，每个采样的tokenid, 都必须发出去，要求必须从GPU拷贝到CPU（worker），然后返回给enginecore, 然后被线程发出去。</p><p>所以，拷贝是必须的。</p><p>原来我们的同步调度，每次发一个batch，就必须等待worker的execute_model执行tokenid采样，同步拷贝到CPU，发出给enginecore输出线程，然后才能继续发下一个batch</p><p>现在我们的异步调度，发送一个batch后， execute_model直接返回，enginecore进程开始准备发送下一个batch（占位+1，所以上一个batch的req decode即便还没有结束，仍然可以继续发射）。</p><p>在worker进程里面，他会收到前后多个batch的任务（这些batch任务，里面req很多都是连续decode），所以worker进程不需要同步阻塞拷贝d2h，来打断这些batch的req 连续decode, 上一个batch的decode结束后，挂一个异步拷贝prev_sampler_token_id的任务，然后直接开始下一个batch里面req的下一个decode的计算。</p><blockquote><p>前一个batch的采样结果出来后，加入到下一次的input_ids占位里面，worker进程直接挂一个异步拷贝的任务拷贝这个原来的任务，然后直接开始下一个batch，开始接着这个采样任务的结果输入进行</p></blockquote><p>所以这样，也不会打断流式输出，也能让一个req的连续decode，不会被GPU-CPU拷贝打断。</p><h6 id="2-异步调度的空批"><a href="#2-异步调度的空批" class="headerlink" title="2. 异步调度的空批"></a>2. 异步调度的空批</h6><p>所谓空batch, 就是 调度器的has_request() &#x3D; True, 说明还有req没跑完，但是这一轮的调度出来的batch，<mark style="background:#fff88f">total_num_scheduled_tokens &#x3D;&#x3D; 0</mark></p><p>这种异步调度的时候，调度出空批的现象，是如何出现的呢？</p><ul><li><h2 id="PP-decode的节流-在异步调度下，调度器里面，当一个req在decode阶段，增加了一个调度的判断逻辑："><a href="#PP-decode的节流-在异步调度下，调度器里面，当一个req在decode阶段，增加了一个调度的判断逻辑：" class="headerlink" title="PP decode的节流  - 在异步调度下，调度器里面，当一个req在decode阶段，增加了一个调度的判断逻辑："></a>PP decode的节流<br>  - 在异步调度下，调度器里面，当一个req在decode阶段，增加了一个调度的判断逻辑：</h2><ul><li><code>if self.current_step &lt; request.next_decode_eligible_step: continue</code></li><li></li><li>就是说，这个req，上一个batch刚发出一个decode，接下来的pp_size次调度，都不能再发req了，(<strong>因为上一个req的decode还没出流水线，stage0如果接到这个req的下一个decode，placeholder是空的，计算不了</strong>)</li></ul></li><li>prefill限流<ul><li>这个下面分析，这个有个defer_prefills时prefill chunk被延后的概念</li></ul></li></ul><p>所以，就是说，这一轮的调度：<br>running队列里面的所有req，都处在上面的两个节流期，<br>waiting队列里面又没新请求可进</p><p><strong>那么本轮的scheduled_running_reqs就是空的。从而就是空批</strong></p><blockquote><p><strong>用你的 Qwen + PP&#x3D;4 举例</strong>：<br>某个请求第 3 步做了 decode，<code>next_decode_eligible_step = 3 + 4 = 7</code>，第 4&#x2F;5&#x2F;6 步都不能再调它。如果当前只有这一个请求、且没有新请求 prefill，那第 4&#x2F;5&#x2F;6 步就是空批。</p></blockquote><p>虽然是空批，但是调度器依然会提交给execute_model (因为只要有has_requests()，调度器就会调度)，但是model_executed&#x3D;False(本轮没有要计算的token)，所以不走采样分支。</p><h6 id="3-defer-prefills"><a href="#3-defer-prefills" class="headerlink" title="3. defer_prefills"></a>3. defer_prefills</h6><p>这个是DP数据并行 prefill均衡机制。</p><p>他决定这一轮要不要延后prefill的计算，把算力让给decode，所以defer_prefills就是个标志位。<br><img src="/images/Pasted%20image%2020260827204734.png" alt="399"></p><p><img src="/images/Pasted%20image%2020260827204803.png" alt="448"></p><p>所以，我在跑PP的时候，defer_prefills 永远是False.</p><h6 id="4-pooling"><a href="#4-pooling" class="headerlink" title="4. pooling"></a>4. pooling</h6><p>pooling在原始含义中，是池化（CNN里面），把一组值聚合成一个值，做下采样</p><p>而在vllm的语境中，pooling模型 &#x3D; <strong>把序列隐藏态， 聚合成一个向量</strong></p><p>所以pooling模型，和生成式模型，两者是同等地位的概念</p><p><img src="/images/Pasted%20image%2020260827205201.png"></p><p><img src="/images/Pasted%20image%2020260827205408.png"><br><img src="/images/Pasted%20image%2020260827205424.png"></p><p><img src="/images/Pasted%20image%2020260827205514.png"></p><p>所以这里的embedding，和我们llm里面的embedding层，不是一个东西，两个的区别如下：</p><p><img src="/images/Pasted%20image%2020260827205651.png"></p><p>文本embedding &#x3D; transformer + pooling<br><img src="/images/Pasted%20image%2020260827205732.png"></p><h6 id="6-ec-consumer-编码器缓存"><a href="#6-ec-consumer-编码器缓存" class="headerlink" title="6. ec consumer 编码器缓存"></a>6. ec consumer 编码器缓存</h6><p>ec &#x3D; encoder cache,指的是多模态的编码器的输出缓存</p><p>多模态模型，LLaVA, Qwen-VL等，由两部分组成：</p><p><img src="/images/Pasted%20image%2020260827210308.png"></p><p><code>encoder_cache</code> 就是 <strong>vision encoder 输出的缓存</strong></p><p><img src="/images/Pasted%20image%2020260827210405.png"></p><blockquote><p>这里的有点像是prefix cache的感觉，图像前缀缓存</p></blockquote><p>EC分离，就是把编码图像，和跑LLM拆到不同的卡上</p><p><img src="/images/Pasted%20image%2020260827210508.png"></p><p>所以，ec consumer， 就是图像编码器输出的消耗者（使用者），所以，就是说，如果我们是多模态模型，那么这个主推理llm，就是要用到这个图像编码器的输出图像。</p><p><img src="/images/Pasted%20image%2020260827210744.png"><br>很显然，我们这里，因为不是多模态，也不是特别用来处理图像的，所以默认是true，表示是消费者，只不过我们跑的是llm，不需要消费图像。</p><h6 id="7-调度器输出的-pending-structured-output-tokens"><a href="#7-调度器输出的-pending-structured-output-tokens" class="headerlink" title="7. 调度器输出的.pending_structured_output_tokens"></a>7. 调度器输出的.pending_structured_output_tokens</h6><p>这个要拆成两部分理解，</p><ul><li><p>structured output &#x3D; 结构化输出</p><ul><li>就是约束llm按照固定格式生成，比如生成json格式语法，直接生成被程序直接解析的东西，而不是自由文本。</li><li>vllm里面使用grammar bitmask实现。采样的时候，用一个位掩码标记vocab里哪些token是当前语法允许的。非法token的logits &#x3D; -inf, 所以模型只能采样到合法token， 从而保证输出始终符合grammar.</li><li>所以，<mark style="background:#fff88f">grammar bitmask本质上是一个状态机</mark></li></ul></li><li><p>pending &#x3D; 缺token在等</p><ul><li>说的是，上一轮tokne还没回来。</li><li>我们这个<strong>pending_structured_output_tokens，本质还是个标志位</strong>。</li><li><img src="/images/Pasted%20image%2020260827211346.png"></li><li>两个条件同时满足；<ul><li>这个req，要求使用结构化输出</li><li>num_output_placeholders &gt; 0,  说明还有token在途（调度出去了，但是采样结果还没回传CPU，所以仍是占位符）</li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260827211707.png"></p><p>这里的延迟采样，就涉及到一个<mark style="background:#fff88f">采样操作的触发时机</mark>了</p><p>我们调度器调度出一个batch, 给worker的model_runner进行执行，推理出logits，到这一步就结束了，</p><p>sampler的采样操作，是需要执行器显式发出第二次RPC &#x3D; sample_tokens 。前向和采样，是两个独立的RPC，严格交替调用。</p><p><img src="/images/Pasted%20image%2020260827212952.png"></p><p>所以，当搞清楚了前向-采样的时机后，再看一下，异步情况下，这个placeholders的占位出现的情况。</p><p>也就是placeholder 占位符的完整生命周期。<br><strong>placeholder 是 scheduler侧的赊账</strong>， 调度的时候+1， 声明这个token在途，输出回来的时候-1核销。</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">时间轴 ───────────────────────────────────────────────────────────────────────→</span><br><span class="line"></span><br><span class="line">        ┌────────────── step N ──────────────┐</span><br><span class="line">Scheduler │ schedule()                        │</span><br><span class="line">(CPU)     │  计算 num_new_tokens = 1          │</span><br><span class="line">          │  _update_after_schedule:          │</span><br><span class="line">          │    placeholder +1  →  占位=1      │</span><br><span class="line">          └──────┬───────────────────────────┘</span><br><span class="line">                 ↓ RPC1 execute_model</span><br><span class="line">Worker          │  forward → compute_logits</span><br><span class="line">(GPU)           │    logits 留 GPU</span><br><span class="line">                 ↓ RPC2 sample_tokens</span><br><span class="line">                │  _sample(logits) → token &quot;好&quot;</span><br><span class="line">                │  token 留 GPU(prev_sampled_token_ids)</span><br><span class="line">                │  ←── 不回 CPU！占位=1 不核销</span><br><span class="line">                │</span><br><span class="line">        ┌────────────── step N+1 ────────────┐</span><br><span class="line">Scheduler │ schedule()                        │</span><br><span class="line">(CPU)     │  num_new_tokens =                │</span><br><span class="line">          │   num_tokens_with_spec(旧)       │</span><br><span class="line">          │   + placeholder(1)               │ ← 靠占位补差额</span><br><span class="line">          │   - num_computed_tokens          │</span><br><span class="line">          │  _update_after_schedule:          │</span><br><span class="line">          │    placeholder +1  →  占位=2      │</span><br><span class="line">          └──────┬───────────────────────────┘</span><br><span class="line">                 ↓ RPC1 + RPC2 (同 step N)</span><br><span class="line">                │  token &quot;好&quot; 仍在 GPU</span><br><span class="line">                │</span><br><span class="line">        ┌─────── step N 的输出终于回来了 ─────┐</span><br><span class="line">Scheduler │ update_from_output()              │</span><br><span class="line">(CPU)     │  收到 step N 的采样 token &quot;好&quot;     │</span><br><span class="line">          │  _update_request_with_output:     │</span><br><span class="line">          │    placeholder -1  →  占位=1      │</span><br><span class="line">          │    num_tokens_with_spec 更新      │</span><br><span class="line">          └───────────────────────────────────┘</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260827213736.png"></p><p>这里复习一下：</p><ul><li>num_tokens_with_spec<ul><li>表示这个req的seq已知token id 的数量(不管有没有kvcache，反正已知token id了)</li></ul></li><li>num_computed_tokens<ul><li>表示这个req的seq, 已经算出kv的token id的数据</li><li>所以我们调度的逻辑就是让num_computed_tokens 去 追赶num_tokens_with_spec</li></ul></li><li>num_output_placeholders<ul><li>表示调度出去，还没有真正返回收到token id的这个虚token id的数量</li></ul></li></ul><p>所以，我们调度计算num_new_tokens (表示本batch中这个req需要计算的token kvcache的token数)</p><p><code>num_new_tokens = num_tokens_with_spec + num_output_placeholders - num_computed_tokens</code></p><ul><li>同步调度的时候<ul><li>没有placeholder, 因为不需要占位</li><li>所以num_new_tokens &#x3D; 已经知道id 的token数  - 已经计算出kvcache的token数</li></ul></li><li>异步调度的时候<ul><li>假设连续发送两次调度。每次都需要记账。</li><li>num_new_tokens 需要额外考虑在途的，这个也算是已知未知的。<br><img src="/images/Pasted%20image%2020260827214443.png"></li></ul></li></ul><hr><p>所以这里再梳理一下，调度器的账单：<br><img src="/images/Pasted%20image%2020260827215105.png"></p><p>下面这个表还是很清晰的，</p><ul><li><strong>placeholder就表示本轮的预计采样的个数</strong></li><li>with_spec表示目前已知id的token数</li><li>computed表示已经知道kv + 已经发出计算kv的 token个数</li><li>num_new_tokens 表示本轮需要计算的token数<br><img src="/images/Pasted%20image%2020260827215418.png"></li></ul><hr><p>所以，这个时候，再回头看看，原来pending_structure_output_token的标志位表示的意思</p><p><code>pending_structured_output_tokens</code> 是<strong>异步调度 + 结构化输出（grammar 约束解码）</strong> 之间冲突的产物</p><p>关键：<strong>算 bitmask 必须知道”已经生成到哪了”的输出 token</strong>，这个语法状态机是 CPU 端逐 token 推进的（<code>structured_output_manager.grammar_bitmask</code>）</p><p><img src="/images/Pasted%20image%2020260827215851.png"></p><p>enginecore, 调度N步，发出batch N 的前向 RPC后，想要立刻发出采样RPC，但是这个时候，如果batch N-1的placeholder的那个token id还不知道（就是batch N 的placeholder &#x3D; 2, 因为batchN-1还没采样完），所以batchN这一轮的采样RPC无法发出。因为依赖bitmask，但是没有更新呢。</p><p>所以，pending_structured_output_tokens &#x3D; True的意思，表示：<br>本轮（batchN）是结构化输出，所以有bitmask要采样RPC发送，但是batchN-1的placeholder还没有核销，所以bitmask未更新，所以推迟采样。</p><p><img src="/images/Pasted%20image%2020260827220542.png"></p><p>我懂了，这里因为我们有了bitmask的结构化输出的需要，所以必须后一步的采样，必须依赖前一步采样的回传更新bitmask才行。</p><p>如果不使用bitmask, 后一步的采样RPC直接发出即可，因为只需要上一步的token已经在GPU侧被计算出且已经被本轮计算成logits，不需要回传再采样。</p><p>所以bitmask的本质，就是把采样，从纯GPU操作，变成了CPU-GPU串行链</p><p><img src="/images/Pasted%20image%2020260827221151.png"></p><h6 id="step-with-batch-queue-异步调度的step总结"><a href="#step-with-batch-queue-异步调度的step总结" class="headerlink" title="step_with_batch_queue 异步调度的step总结"></a>step_with_batch_queue 异步调度的step总结</h6><p>这个是异步调度的总结</p><p>好的，我现在已经trace完了step_with_batch_queue, 简单总结一下，就是，</p><ul><li><code>step_with_batch_queue</code> 一次调用只做<strong>其中一件事</strong>：<ul><li><strong>push 型 step</strong>：调度一个新 batch，发前向+采样 RPC，塞进 queue，直接 return（不碰结果）。</li><li><strong>pop 型 step</strong>：不调度新 batch，pop 队首，阻塞等采样结果，核销。</li></ul></li></ul><p>它是”<strong>填队列</strong>“和”<strong>掏队列</strong>“交替的循环，不是每次都跑前向。</p><blockquote><p>这个step_with_batch_queue, 里面的状态有两个约束条件:<br>一个是batch_queue, 一个是调度器has_requests() 所以每次进入这个step_with_batch_queue, 主要处于两个过程： 一种是batch_queue还没满，这个时候，就是当前batch发出前向RPC，然后直接发出采样RPC。然后把这个batch加入batch_queue后直接返回 另一种是batch_queue已经满了，这个时候，就是最后一个batch被加入batch_queue后，不返回，开始处理batch_queue的最老的batch，pop弹出阻塞等待这个batch的采样RPC结果返回。然后核销调度器的账单。</p></blockquote><p><strong>分支1（填队列）</strong> 的 return 条件不是”队列没满”这么简单，而是（core.py:1032）：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(batch_queue) &lt; <span class="variable language_">self</span>.batch_queue_size <span class="keyword">and</span> (</span><br><span class="line">    model_executed <span class="keyword">or</span> <span class="variable language_">self</span>.scheduler.has_requests()</span><br><span class="line">):</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span>, model_executed</span><br></pre></td></tr></table></figure><p>两个条件缺一不可：</p><ol><li>队列没满 <strong>且</strong></li><li>还有活可干（本批真算了，或调度器还有 req）</li></ol><p>如果”队列没满但调度器已经空了”，就<strong>不会 return</strong>，而是掉下去 pop 掏队列——因为该收尾了。</p><p><strong>分支2（掏队列）</strong> 的触发，不只是”队列满了”，而是”<strong>没有提前 return</strong>“的所有情况，包括：</p><ul><li>队列 append 后满了（<code>len == size</code>）</li><li>队列没满，但调度器空了（没有新活可填）</li><li>唯一例外：<code>elif not batch_queue</code> → 调度器空且队列也空 → return None（纯空转）</li></ul><p><strong>还有一个你已经知道的第三路径：deferred</strong></p><p>结构化输出 pending 时，这步<strong>不 append、不 return</strong>，直接掉下去 pop 队首（等上一批 token 回来），核销完再补采样 deferred batch。这打破了你”填&#x2F;掏二选一”的框架——它是”<strong>既填了前向、又掏了队列</strong>“的混合。</p><p><img src="/images/Pasted%20image%2020260827225153.png"></p><h6 id="step-和-调度器对应关系"><a href="#step-和-调度器对应关系" class="headerlink" title="step 和 调度器对应关系"></a>step 和 调度器对应关系</h6><p>vllm里面，step_fn的选择，以及调度器的选择，是两个独立的配置选项</p><ul><li><mark style="background:#fff88f">调度器的选择，根据：async_scheduling决定</mark><ul><li>Scheduler</li><li>AsyncScheduler</li></ul></li><li><mark style="background:#fff88f">step_fn的选择，根据max_concurrent_batches 的最大并行batch数选择</mark><ul><li>无batch_queue, step(), 表示一次只有一个batch在运行</li><li>有batch_queue, step_with_batch_queue()， 表示一次有多个batch在运行</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260827230247.png"></p><p>是因为为了利用好PP的流水线并行的好处，我们肯定要好几个batch一起在运行才能显示出PP的好处，不然一次就一个batch在同时运行，PP开了和没开一样</p><p><img src="/images/Pasted%20image%2020260827230434.png"></p><p><img src="/images/Pasted%20image%2020260827230445.png"></p><h6 id="AsyncScheduler-和-Scheduler的区别"><a href="#AsyncScheduler-和-Scheduler的区别" class="headerlink" title="AsyncScheduler 和 Scheduler的区别"></a>AsyncScheduler 和 Scheduler的区别</h6><p><strong>AsyncScheduler 只 override 了两个”钩子方法”，核心调度决策完全继承、一字未改</strong>。区别全在这两个钩子做的”在途记账”上</p><p><img src="/images/Pasted%20image%2020260827230802.png"></p><p>所以区别就在于，有没有维护placeholder的项</p><p><img src="/images/Pasted%20image%2020260827230940.png"></p><p>PP 要的是<strong>执行层</strong>（多 batch 填流水线），它<strong>不强制要求</strong>记账层（token 留 GPU）。所以：</p><ul><li><p><strong>纯 PP（组合②）</strong> &#x3D; 开执行层、不开记账层 &#x3D; <code>普通 Scheduler + step_with_batch_queue</code>。placeholder 恒 0，token 照旧回 CPU，但多个 batch 同时在 GPU 上不同阶段跑，流水线照样填满。</p></li><li><p><strong>Async（组合③④）</strong> &#x3D; 执行层 + 记账层全开 &#x3D; <code>AsyncScheduler + step_with_batch_queue</code>。token 也不落 CPU 了，把 CPU 回传的延迟也省掉。</p></li></ul><p><strong>一句话总结</strong></p><p><strong>普通 Scheduler 和 AsyncScheduler 是”同一个调度器 + 一个可选的在途记账插件”</strong>。它们共用同一套 <code>schedule()</code> 决策，唯一区别是 AsyncScheduler 会维护 <code>num_output_placeholders</code>（在途 token 账）。而”记账”和”执行（batch_queue）”是两件独立的事，所以能各自开关、自由组合——PP 只要执行层，async 才两个都要。</p><p><mark style="background:#fff88f">所以就是Scheduler调度器，是同步记账，他还在死等这个req返回，不然不会调度（因为num_with_spec &#x3D; num_computed了）， 但是AsyncScheduler调度器，是异步记账，他会记录下在途的placeholder，这样，下一次这个req，虽然还是num_with_spec &#x3D; num_computed， 但是由于有placeholder，这个req还是可以调度的。</mark></p><p><img src="/images/Pasted%20image%2020260827231453.png"><br><img src="/images/Pasted%20image%2020260827232627.png"></p><p>可以看到，他这里就是仅仅多了维护placeholder的在途记账+回收销账的部分。</p><p><mark style="background:#fff88f">所以，我们的PP流水线并行优化，就要使用AsyncScheduler + step_with_batch_queue, 才能发挥PP的优势</mark></p><p><img src="/images/Pasted%20image%2020260827233052.png"></p><p><img src="/images/Pasted%20image%2020260827233418.png"></p><p><img src="/images/Pasted%20image%2020260827233445.png"></p><p><img src="/images/Pasted%20image%2020260827233505.png"></p><h6 id="8-调度器-model-runner-vx-pp-decode节流"><a href="#8-调度器-model-runner-vx-pp-decode节流" class="headerlink" title="8. 调度器-model_runner vx - pp decode节流"></a>8. 调度器-model_runner vx - pp decode节流</h6><p>这几个是不同层的独立的选择</p><p><mark style="background:#fff88f">3个独立的配置：</mark></p><p><img src="/images/Pasted%20image%2020260827234223.png"></p><p><strong>这三层是”各自开关、自由组合”的</strong>，不要一对一绑定。你之前纠结的”调度器 vs step”就是层1和层2的混淆——它们正交。</p><p>下面有<strong>两个节流</strong>（这两个节流，都在Scheduler里面），<strong>本质就是xxx条件下req不调度的条件</strong></p><p><img src="/images/Pasted%20image%2020260827234423.png"></p><p><img src="/images/Pasted%20image%2020260827234531.png"></p><p>这里就要区分一下model runner v1, v2的区别了</p><p>model_runner v1, 和 v2的区别，主要在于NCCL广播的发送接受时机不同。</p><ul><li>v1<ul><li>主要是内联广播：同步collective</li></ul></li><li>v2<ul><li>异步广播</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260828104005.png"></p><p><strong>节流是 V2 异步广播的”对价”</strong>：V2 用旁路流把广播藏进计算里（省 bubble），但代价是 token 要 pp_size 步后才就位，所以必须用 <code>next_decode_eligible_step</code> 约束调度器”别排太快”。V1 的广播是同步的、token 立刻到位，反而不用节流——但它那个同步点本身就是 bubble。</p><hr><p><mark style="background:#ff4d4f">这里我让codex也来总结了一下：</mark></p><p>先给结论：</p><ol><li>同一个 decode request 能否连续进入两个 batch，取决于 Scheduler&#x2F;ModelRunner 组合。<ul><li><strong>同步 Scheduler</strong>：一般不能连续 decode；必须等前一批 token 回到 Scheduler。</li><li>AsyncScheduler + ModelRunner V1：可以连续发射，没有 PP decode throttle。</li><li><strong>AsyncScheduler + ModelRunner V2 + PP</strong>：不能连续；同一 request 两次 decode 至少间隔 <code>pp_size</code> 个 scheduler step。</li></ul></li><li>EngineCore 能发很多 RPC，不代表一个 ModelRunner 并发执行很多 batch。RPC 只是排进 worker 的 FIFO 队列；<mark style="background:#fff88f">每个 worker 的 ModelRunner 依旧串行执行</mark>。</li><li>PP 的并行是不同 PP stage 同时处理不同 batch，而不是单卡同时跑多个 forward。</li><li><mark style="background:#fff88f">V1+PP 的完整流水主要适用于同步 Scheduler</mark>。V1+PP+AsyncScheduler 虽然能跑，但 sampled-token broadcast 会阻塞前级，因此代码明确说“<strong>不完整支持</strong>”。</li></ol><p>下面来具体解释一下：</p><h6 id="8-1-同步调度器：不能同一个req连续发射两个decode-batch"><a href="#8-1-同步调度器：不能同一个req连续发射两个decode-batch" class="headerlink" title="8.1 同步调度器：不能同一个req连续发射两个decode batch"></a>8.1 同步调度器：不能同一个req连续发射两个decode batch</h6><p>Scheduler 发射一个 decode token 后，会立刻乐观增加：</p><figure class="highlight fix"><table><tr><td class="code"><pre><span class="line"><span class="attr">request.num_computed_tokens +</span>=<span class="string"> num_scheduled_token</span></span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/core/sched/scheduler.py:2358">scheduler.py (line 2358)</a>。</p><p>假设 request 当前：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">num_tokens</span> = <span class="number">101</span></span><br><span class="line"><span class="attr">num_computed_tokens</span> = <span class="number">100</span></span><br></pre></td></tr></table></figure><p>第一次 decode 调度 1 token：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">num_computed_tokens</span> = <span class="number">101</span></span><br><span class="line"><span class="attr">num_tokens</span> = <span class="number">101</span></span><br></pre></td></tr></table></figure><p>在采样结果回来并追加到 <code>num_tokens</code> 之前：</p><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">num_new_tokens <span class="operator">=</span> num_tokens - num_computed_tokens <span class="operator">=</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>因此下一次 <code>schedule()</code> 不会再次 decode 它。</p><p>所以同步调度时：</p><figure class="highlight isbl"><table><tr><td class="code"><pre><span class="line"><span class="variable">batch</span> <span class="variable">A</span>: <span class="function"><span class="title">decode</span>(<span class="variable">req0</span>)</span></span><br><span class="line"><span class="variable">batch</span> <span class="variable">B</span>: 不能再次 <span class="function"><span class="title">decode</span>(<span class="variable">req0</span>)</span></span><br><span class="line"><span class="variable">output</span> <span class="variable">A</span> 返回</span><br><span class="line"><span class="variable">batch</span> <span class="variable">C</span>: <span class="function"><span class="title">decode</span>(<span class="variable">req0</span>)</span></span><br></pre></td></tr></table></figure><p>但 prefill chunk 是例外。因为 prompt 后面还有未计算 token，所以同一个 request 可以连续进入多个 prefill batch。测试里也明确验证了这一点：<a href="/Users/liangji/Master/project/2_infra/vllm/tests/v1/engine/test_engine_core.py:300">test_engine_core.py (line 300)</a>。</p><h6 id="8-2-异步调度器-model-runner-v1-可以连续发射，但是不完整适配"><a href="#8-2-异步调度器-model-runner-v1-可以连续发射，但是不完整适配" class="headerlink" title="8.2 异步调度器+model_runner v1(可以连续发射，但是不完整适配)"></a>8.2 异步调度器+model_runner v1(可以连续发射，但是不完整适配)</h6><p>异步调度器支持在途记账，所以可以连续发射req，但是由于里面这个节流标志指定v2的引擎，所以，没有节流，<strong>因此真的能做到连续发射req了</strong></p><p>AsyncScheduler 在发射 decode 后增加：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">request</span>.num_output_placeholders += <span class="number">1</span></span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/core/sched/async_scheduler.py:19">async_scheduler.py (line 19)</a>。</p><p>于是下一次调度计算的是：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line">num_new_tokens = (</span><br><span class="line">    request<span class="selector-class">.num_tokens_with_spec</span></span><br><span class="line">    + request<span class="selector-class">.num_output_placeholders</span></span><br><span class="line">    - request<span class="selector-class">.num_computed_tokens</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>placeholder 代表“虽然真实 token 还没有回来，但先假设它将存在”。因此同一个 request 可以这样：</p><figure class="highlight livescript"><table><tr><td class="code"><pre><span class="line">batch A: decode<span class="function"><span class="params">(req0)</span> -&gt;</span> placeholder +<span class="number">1</span></span><br><span class="line">batch B: decode<span class="function"><span class="params">(req0)</span> -&gt;</span> 再增加一个 placeholder</span><br></pre></td></tr></table></figure><p>本地测试直接覆盖了这个行为：</p><figure class="highlight lua"><table><tr><td class="code"><pre><span class="line"><span class="built_in">output</span> = scheduler.schedule()</span><br><span class="line"><span class="built_in">assert</span> req.request_id <span class="keyword">in</span> <span class="built_in">output</span>.num_scheduled_tokens</span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/tests/v1/core/test_scheduler.py:226">test_scheduler.py (line 226)</a>。</p><p>V1 没有设置 <code>next_decode_eligible_step</code>，所以没有 PP cadence throttle。</p><h6 id="8-3-异步调度器-model-runner-v2-pp"><a href="#8-3-异步调度器-model-runner-v2-pp" class="headerlink" title="8.3 异步调度器 + model_runner v2 + pp"></a>8.3 异步调度器 + model_runner v2 + pp</h6><p>因为指定了v2，所以打开了pp decode节流</p><p>V2 每次发射 decode 后会设置：</p><figure class="highlight crystal"><table><tr><td class="code"><pre><span class="line">request.next_decode_eligible_step =</span><br><span class="line">    <span class="keyword">self</span>.current_step + <span class="keyword">self</span>.pp_size</span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/core/sched/async_scheduler.py:57">async_scheduler.py (line 57)</a>。</p><p>Scheduler 在后面的 step 中检查：</p><figure class="highlight gml"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="symbol">self</span>.current_step &lt; request.next_decode_eligible_step:</span><br><span class="line">    <span class="keyword">continue</span></span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/core/sched/scheduler.py:864">scheduler.py (line 864)</a>。</p><p>例如 PP&#x3D;2：</p><figure class="highlight arduino"><table><tr><td class="code"><pre><span class="line">step T:     <span class="built_in">decode</span>(req0)</span><br><span class="line">step T<span class="number">+1</span>:   req0 被 throttle</span><br><span class="line">step T<span class="number">+2</span>:   req0 才能再次 decode</span><br></pre></td></tr></table></figure><p>准确总结：</p><table><thead><tr><th>模式</th><th>同 req 连续 decode</th></tr></thead><tbody><tr><td>Sync Scheduler</td><td>否</td></tr><tr><td>Async + V1</td><td>是</td></tr><tr><td>Async + V2, PP&#x3D;1</td><td>可以每步</td></tr><tr><td>Async + V2, PP&#x3D;P</td><td>至少间隔 P 步</td></tr></tbody></table><h6 id="8-4-modelrunner只能跑一个batch-enginecore可以发很多RPC"><a href="#8-4-modelrunner只能跑一个batch-enginecore可以发很多RPC" class="headerlink" title="8.4 modelrunner只能跑一个batch, enginecore可以发很多RPC"></a>8.4 modelrunner只能跑一个batch, enginecore可以发很多RPC</h6><p><code>step_with_batch_queue()</code> 会按 batch 发两条 RPC：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">execute_model</span><span class="params">(batch A)</span></span></span><br><span class="line"><span class="function"><span class="title">sample_tokens</span><span class="params">(batch A)</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">execute_model</span><span class="params">(batch B)</span></span></span><br><span class="line"><span class="function"><span class="title">sample_tokens</span><span class="params">(batch B)</span></span></span><br></pre></td></tr></table></figure><p>代码位置：</p><ul><li>发 execute：<a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/engine/core.py:958">core.py (line 958)</a></li><li>发 sample：<a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/engine/core.py:1001">core.py (line 1001)</a></li><li>放入 batch queue：<a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/engine/core.py:1022">core.py (line 1022)</a></li><li>队列满后等待最老结果：<a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/engine/core.py:1065">core.py (line 1065)</a></li></ul><p>但 MultiprocExecutor 的 <code>non_block=True</code> 只做了两件事：</p><ol><li>把 RPC 放进广播 MessageQueue。</li><li>返回一个 Future。</li></ol><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/executor/multiproc_executor.py:587">multiproc_executor.py (line 587)</a>。</p><p>Future 甚至没有后台线程主动帮你执行 <code>get_response()</code>。调用 <code>future.result()</code> 时，才按 FIFO drain 之前的响应：</p><figure class="highlight oxygene"><table><tr><td class="code"><pre><span class="line"><span class="keyword">while</span> <span class="keyword">not</span> <span class="keyword">self</span>.done():</span><br><span class="line">    <span class="keyword">future</span> = <span class="keyword">self</span>.futures_queue.pop()</span><br><span class="line">    <span class="keyword">future</span>._wait_for_response()</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260828145048.png" alt="546"></p><p><strong>RPC 可以大量 pending，但 ModelRunner 调用仍是串行的</strong>。</p><h6 id="8-5-v1-model-runner-的-PP流水线是怎么跑起来的"><a href="#8-5-v1-model-runner-的-PP流水线是怎么跑起来的" class="headerlink" title="8.5 v1 model_runner 的 PP流水线是怎么跑起来的"></a>8.5 v1 model_runner 的 PP流水线是怎么跑起来的</h6><p>先以 <code>PP=2, TP=1, sync scheduler</code> 为例。</p><p>每次 RPC 会广播给所有 worker，所以两个 stage 都收到完全相同的 RPC 序列：</p><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line"><span class="built_in">execute</span>(A), <span class="built_in">sample</span>(A), <span class="built_in">execute</span>(B), <span class="built_in">sample</span>(B)</span><br></pre></td></tr></table></figure><p>但它们在 <code>execute(A)</code> 中做的事情不同。</p><p><mark style="background:#fdbfff">PP stage 0</mark></p><p>Stage 0：</p><ol><li>准备 input ids、position、KV slot。</li><li>执行前半部分 transformer layers。</li><li>返回 <code>IntermediateTensors</code>。</li><li>GPUWorker 用 <code>isend_tensor_dict()</code> 非阻塞发送给 stage 1。</li><li>不计算 logits，不采样。</li></ol><p>模型层切分可参考 Llama：</p><ul><li>只有 first PP rank 有 embedding。</li><li>每个 rank 只执行 <code>[start_layer, end_layer)</code>。</li><li>非 last rank 返回 <code>hidden_states + residual</code>。</li><li>只有 last rank 执行 norm&#x2F;logits。</li></ul><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/model_executor/models/llama.py:370">llama.py (line 370)</a> 和 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/model_executor/models/llama.py:400">llama.py (line 400)</a>。</p><p>GPUWorker 收到中间张量后进行非阻塞发送：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">self._pp_send_work</span> = get_pp_group().isend_tensor_dict(...)</span><br></pre></td></tr></table></figure><p>见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/worker/gpu_worker.py:1430">gpu_worker.py (line 1430)</a>。</p><p><mark style="background:#fdbfff">PP stage 1</mark></p><p>Stage 1：</p><ol><li><code>irecv_tensor_dict()</code> 接收 stage 0 中间张量。</li><li>执行后半部分 layers。</li><li>计算 logits。</li><li>把 logits 放入 <code>execute_model_state</code>。</li><li>下一条 <code>sample(A)</code> RPC 消费状态并采样。</li></ol><p>只有最后一个 PP stage 的 TP rank 0 把 <code>ModelRunnerOutput</code> 返回 EngineCore。这个 rank 的计算见 <a href="/Users/liangji/Master/project/2_infra/vllm/vllm/v1/executor/multiproc_executor.py:766">multiproc_executor.py (line 766)</a>。</p><p> sync + V1 + PP&#x3D;2 时间线</p><figure class="highlight less"><table><tr><td class="code"><pre><span class="line">时间 ─────────────────────────────────────────────&gt;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">PP0</span>:  <span class="selector-tag">forward</span> <span class="selector-tag">A</span> ──<span class="selector-tag">send</span> <span class="selector-tag">A</span>── <span class="selector-tag">forward</span> <span class="selector-tag">B</span> ──<span class="selector-tag">send</span> <span class="selector-tag">B</span>── <span class="selector-tag">forward</span> <span class="selector-tag">C</span></span><br><span class="line">                         ↑</span><br><span class="line"><span class="selector-tag">PP1</span>:              <span class="selector-tag">recv</span> <span class="selector-tag">A</span>──<span class="selector-tag">forward</span> <span class="selector-tag">A</span>/<span class="selector-tag">sample</span> <span class="selector-tag">A</span></span><br><span class="line">                                      <span class="selector-tag">recv</span> <span class="selector-tag">B</span>──<span class="selector-tag">forward</span> <span class="selector-tag">B</span>/<span class="selector-tag">sample</span> <span class="selector-tag">B</span></span><br><span class="line"></span><br><span class="line"><span class="selector-tag">EngineCore</span>:</span><br><span class="line">      <span class="selector-tag">enqueue</span> <span class="selector-tag">A</span></span><br><span class="line">      <span class="selector-tag">enqueue</span> <span class="selector-tag">B</span></span><br><span class="line">      <span class="selector-tag">wait</span> <span class="selector-tag">output</span> <span class="selector-tag">A</span></span><br><span class="line">      <span class="selector-tag">enqueue</span> <span class="selector-tag">C</span></span><br></pre></td></tr></table></figure><p>关键是同步模式下，非 last PP rank 的 <code>sample(A)</code> 基本只是空操作：</p><ul><li>stage 0 在 execute 时已经返回 <code>IntermediateTensors</code>；</li><li>没有建立 <code>execute_model_state</code>；</li><li><code>sample_tokens()</code> 看到 state 为 None 后直接返回。</li></ul><p>所以 stage 0 可以迅速继续执行 <code>execute(B)</code>，与此同时 stage 1 正在执行 A。这才形成真正的 PP overlap。</p><hr><h6 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h6><p>目前我们这里有3个方面的东西</p><ul><li><p><mark style="background:#fdbfff">step_with_batch_queue</mark></p><ul><li>这是异步调度下，batch_queue的步进方法。</li><li>他的作用就是始终维护worker们，在处理一个batch_queue的任务队列。</li></ul></li><li><p><mark style="background:#fdbfff">调度器</mark></p><ul><li>同步调度器<ul><li>规则：必须接收到worker返回的采样token，同一个req才会被再次调度。<ul><li>所以，他保证batch_queue里面，同一个req，绝对不可能同时出现在queue里面的两个batch</li></ul></li></ul></li><li>异步调度器<ul><li>支持在途记账 + PP decode 节流（仅限V2 model_runner）</li><li>搭配v1 model_runner 使用，无节流功能：<ul><li>所以可以连续发送req，batch_queue里面，可以出现两个batch，拥有同一个req。<ul><li><mark style="background:#fff88f">但是由于是model runner v1, 所以，每个pp stage 的rank 执行完对应的batch后，会阻塞等待尾部rank广播返回采样token后，才会开始执行下一个batch的前向</mark></li></ul></li></ul></li><li>搭配v2 model_runner 使用，开启节流功能：<ul><li>保证了可以连续发送同一个req，但是在batch_queue里面，拥有同一个req的batch不会同时出现在PP的pipeline里面</li></ul></li></ul></li></ul></li><li><p>model_runner</p><ul><li>v1<ul><li><img src="/images/Pasted%20image%2020260828162213.png" alt="316"></li><li><img src="/images/Pasted%20image%2020260828162239.png" alt="332"></li></ul></li><li>v2<ul><li><img src="/images/Pasted%20image%2020260828162350.png"></li></ul></li></ul></li></ul><h6 id="9-ppx-stage之间的卡间通信机制"><a href="#9-ppx-stage之间的卡间通信机制" class="headerlink" title="9. ppx stage之间的卡间通信机制"></a>9. ppx stage之间的卡间通信机制</h6><p><mark style="background:#ff4d4f">1. p2p通信</mark><br>当pp0 算完batchA 后，需要把batchA的hidden_state 发送给 pp1处理，pp0自己则开始处理batchB</p><p>发送hidden_state的方法是isend, irecv.</p><ul><li><strong>pp0发送：isend:</strong><ul><li>先发送metadata，后发送tensor数据，非阻塞，直接返回发送句柄handle</li><li>等pp0开始处理batchB的时候，发送句柄handle.wait()阻塞。</li></ul></li><li><strong>pp1接受：irecv:</strong><ul><li>先接受metadata, 然后自己创建好张量空间。</li><li>创建接受句柄handle, 包装成一个异步张量AsyncIntermediate Tensors。 直接开始forward batchA</li><li>当真的开始访问这个张量了，开始handle.wait阻塞等待。</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260828194607.png" alt="400"><br><img src="/images/Pasted%20image%2020260828194630.png" alt="362"></p><p>而我们的TP，就是isend的时候不需要先合并发送完整的，可以直接发送各个tp组的切片即可</p><p><img src="/images/Pasted%20image%2020260828195715.png"></p><p><mark style="background:#ff4d4f">2. model_runner v1 里面的 尾rank 采样token广播</mark></p><p><img src="/images/Pasted%20image%2020260828194808.png" alt="206"></p><p>为什么需要广播，因为在搭配异步调度器AsyncScheduler的时候，连续两个batch，是可以包含同一个req的。</p><p>假设batchA, batchB包含同一个request</p><p><img src="/images/Pasted%20image%2020260828195005.png" alt="272"></p><p>负责处理batchB的pp0不知道，但是尾rank只是把这个采样token传回了EngineCore来更新调度器，但是batchB已经发出去了。<br><img src="/images/Pasted%20image%2020260828195108.png" alt="314"></p><p>所以，<mark style="background:#fff88f">model_runner v1 额外添加GPU直接广播</mark></p><p>尾rank 直接广播给所有非尾rank, 这些非尾rank收到后，就把这个sampled_token给记录在他们要处理的batch的input_batch.prev_sampled_token_ids里面</p><blockquote><p>这里其实尾rank 单独发给 pp0就可以了，但是vllm广播给所有rank，是为了让每个pp worker的各自维护的inputbatch状态副本保持一致。<mark style="background:#fff88f">相当于修改全局</mark></p></blockquote><p><img src="/images/Pasted%20image%2020260828200357.png"></p><ul><li>尾rank 广播：<ul><li>默认stream上：forward A -&gt; torch.distributed.broadcast - &gt; forward B操作（等非尾传过来）</li></ul></li><li>非尾rank 接受广播：<ul><li>默认stream上，在forward A -&gt;sample A(torch.distributed.broadcast 阻塞等待) - &gt; forward B</li></ul></li></ul><p>所以v1的广播，导致的是cuda流上的阻塞，你cpu可能没有阻塞（看你广播发送接受的async_op指定是否cpu也同步阻塞）， 但是不管cpu阻不阻塞，cuda流肯定是阻塞住了，导致v1容易出现很多的bubble</p><p><img src="/images/Pasted%20image%2020260828200813.png" alt="583"></p><p>而且 v1的broadcast 和 irecv, isend 的 p2p 是共用 pp.device_group 的。所以是同一个PP NCCL communicator.</p><p><mark style="background:#fff88f">所以一个PP组里面的通信，广播还会和p2p争抢，又会出现阻塞</mark>。</p><p>所以model_runner里面，<strong>出现两个地方阻塞，导致bubble的出现</strong>。</p><blockquote><p>不仅可能在同一 main stream 上排序，还共享 communicator 的操作顺序，更难独立推进</p></blockquote><p><mark style="background:#ff4d4f">3. model_runner v2 的旁路流broadcast</mark></p><p>model_runner v2 为了解决这些冲突，做了三个改动：</p><ul><li>独立的CUDA stream</li><li>独立的NCCL communicator</li><li>pp decode 节流</li></ul><p><mark style="background:#d3f8b6">改变1：独立cuda stream</mark></p><ul><li>main_stream<ul><li>给计算forward用的</li></ul></li><li>broadcast_stream<ul><li>给广播专用</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260828201411.png" alt="472"></p><p><strong>在发送broadcast A 的stream任务前，先让这个stream阻塞等待main_stream的采样结果出来</strong><br><img src="/images/Pasted%20image%2020260828201602.png" alt="426"></p><p>依赖方向只有：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">main</span> sample <span class="selector-tag">A</span></span><br><span class="line">    → broadcast stream send <span class="selector-tag">A</span></span><br></pre></td></tr></table></figure><p>没有反向要求 main stream 立即等 broadcast：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line">broadcast <span class="selector-tag">A</span></span><br><span class="line">    ↛ <span class="selector-tag">main</span> stream 立即等待</span><br></pre></td></tr></table></figure><p>所以尾 stage 可以在提交 broadcast 后，让 main stream 继续处理后面的 forward。</p><p><mark style="background:#fff88f">关于非尾接受</mark></p><p><img src="/images/Pasted%20image%2020260828201706.png" alt="404"></p><p>此时 token A 可能还没到，但没关系：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">main</span> stream 可以继续 forward 不依赖 token <span class="selector-tag">A</span> 的 batch</span><br></pre></td></tr></table></figure><p><mark style="background:#d3f8b6">改变二： 独立NCCL communicator</mark></p><p><img src="/images/Pasted%20image%2020260828202122.png" alt="493"></p><p>为广播的发送调度通路，再创建一个相同成员的调度网络</p><p>所以 V2 有两条独立 NCCL 通道：</p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">PP <span class="params">device_group:</span></span><br><span class="line">    activation isend<span class="symbol">/irecv</span></span><br><span class="line"></span><br><span class="line">PP <span class="params">broadcast_group:</span></span><br><span class="line">    sampled-token broadcast</span><br></pre></td></tr></table></figure><p>这很重要。只有独立 CUDA stream 但还共用同一个 communicator，collective&#x2F;P2P 仍可能受到 communicator 操作顺序影响。</p><blockquote><p>所以，到目前位置， <strong>model_runner v2做了 不同stream + 不同 communicator</strong></p><p>才能真正允许 activation P2P 和 sampled-token broadcast 并行推进。</p></blockquote><p><mark style="background:#affad1">v2改变三：decode 节流 &#x3D; 延迟 pp_size 步消费</mark></p><p><img src="/images/Pasted%20image%2020260828202337.png" alt="479"></p><p>这边是调度节流</p><p>model_runner里面其实还有 非尾rank旁路记账接受采样token + event标签， 这样在main_stream里如果碰上了节流后的同一个req，才阻塞等待之前记账的event标签，等到了说明旁路记账的采样token真的被广播接收到了。</p><p>所以每次execute_model的前向开头，会</p><figure class="highlight pf"><table><tr><td class="code"><pre><span class="line">先把 T 的真实采样结果写入本地 request <span class="keyword">state</span></span><br><span class="line">再处理 T+P 的调度增量</span><br><span class="line">最后准备 T+P 的 input ids</span><br></pre></td></tr></table></figure><p>否则同一个 request 在 T+P 再次 decode 时，非尾 stage 仍然只有 placeholder，不知道真实 token。先更新，在前向同一个req</p><p>Scheduler throttle 和 PPHandler FIFO 是严格配套的：</p><p><img src="/images/Pasted%20image%2020260828204118.png" alt="513"><br><img src="/images/Pasted%20image%2020260828204325.png" alt="483"></p><blockquote><p>注意，<mark style="background:#fff88f">这边有一个小巧思</mark>，他设计一个旁路记账当前batch的一个采样结果接受，然后把他保存到FIFO里面，我们调度器的节流，保证相同的req最早第二次出现是在pp_size步之后，而每次execute_model，开头就先做一次FIFO弹出，目的是定时回收更新inputbatch持久化状态批。但是他只要保证这个包含后面重复req的batch的采样结果，一定在这个同样req出现之前被更新到inputbatch就行了。</p><p><mark style="background:#fff88f">所以FIFO步，其实也是配合节流节奏的延迟更新。他反正只要比相同req早就行了</mark></p></blockquote><p><mark style="background:#d2cbff">我彻底理解了，节流 + 旁路记账采样结果接受</mark></p><p><mark style="background:#fff88f">所以他这里，main_stream里面，不是永远不阻塞等待之前batch的采样结果，而是保证在T+ pp_size步（一定会在同样req出现前）时，加入wait_event，main_stream阻塞住等待结果。</mark></p><p><mark style="background:#fff88f">所以这里还是有阻塞等待的，但是不是像v1里面，每个batch 的 foward都在阻塞等待当前的batch的结果。<br>这里是阻塞等待pp_size步前的batch的采样结果，这样的好处就是把等待采样结果的实际和实际推进前向的时间重叠了pp_size步</mark></p><hr><h3 id="EP"><a href="#EP" class="headerlink" title="EP"></a>EP</h3><p>终于开始专家并行了。</p><h4 id="MoE架构"><a href="#MoE架构" class="headerlink" title="MoE架构"></a>MoE架构</h4><p>一个典型 MoE 的 FFN&#x2F;MLP 层可以理解成：</p><p>x→Router(Wgate)→Softmax→TopK→Experts→加权合并x \rightarrow Router(W_{gate}) \rightarrow Softmax \rightarrow TopK \rightarrow Experts \rightarrow 加权合并</p><p>具体来说：</p><ol><li><p>输入 token hidden state：</p><p> x∈Rdmodelx\in \mathbb{R}^{d_{model}}</p></li><li><p>Router 做一次 Linear：</p><p> logits&#x3D;xWgatelogits &#x3D; xW_{gate}</p><p> 如果有 NN 个专家，那么输出就是 NN 个分数。</p></li><li><p>对这些专家分数做 softmax，得到 routing probability：</p><p> p&#x3D;softmax(logits)p&#x3D;\text{softmax}(logits)</p></li><li><p>选出 Top-K 专家。比如 <code>top_k=2</code>，那么一个 token 会选择两个专家，同时得到：</p><ul><li>专家 ID，例如 <code>[Expert 3, Expert 7]</code></li><li>对应权重，例如 <code>[0.7, 0.3]</code></li></ul></li><li><p>把这个 token 的 hidden state xx 交给选中的专家：</p><p> y3&#x3D;Expert3(x),y7&#x3D;Expert7(x)y_3&#x3D;Expert_3(x),\quad y_7&#x3D;Expert_7(x)</p></li><li><p>最后根据 router 权重加权求和：</p><p> y&#x3D;0.7y3+0.3y7y &#x3D; 0.7y_3+0.3y_7</p></li></ol><blockquote><p><mark style="background:#d2cbff">MoE 一般只是把原来 Transformer block 里的 <strong>MLP&#x2F;FFN 子层替换成 MoE FFN</strong>，并不会把 RMSNorm 和残差结构去掉</mark></p></blockquote><p>至于每个专家，是双层MLP，还是SwiGLU + MLP。具体结构取决于模型</p><p><img src="/images/Pasted%20image%2020260831204538.png"></p><p>所以你可以理解成：</p><ul><li>Router 的 <code>W_gate</code>：是用来<strong>选专家</strong>的。</li><li>Expert 里面的 <code>gate_proj</code>：是 <strong>SwiGLU 本身的一部分</strong>。</li></ul><p>这两个虽然名字里都有 <code>gate</code>，但完全不是一个东西。</p><img src="/images/Pasted%20image%2020260831204600.png" alt="Pasted image 20260831204600.png" width="328" loading="lazy">![](/images/Pasted%20image%2020260831205013.png)#### EP 专家并行概述<p><img src="/images/Pasted%20image%2020260831204828.png"></p><p>EP并行的挑战：</p><ul><li>跨节点通信开销<ul><li>因为一个rank负责一部分专家</li></ul></li><li>负载均衡挑战<ul><li>EP和DP， TP一起使用，不同分布导致命中专家数量不均衡</li></ul></li></ul><blockquote><p>vllm里面的<strong>EPLB</strong>，用于在使用EP的MoE模型中维护专家负载状态，并配合冗余物理专家等机制缓解热点专家问题。</p></blockquote><blockquote><p>在MoE模型的推理中，DP + EP 是比 纯TP，或者DP + TP + EP更加经典且高效的策略</p></blockquote><p><img src="/images/Pasted%20image%2020260831205744.png"></p><p><img src="/images/Pasted%20image%2020260831205821.png"></p><p><strong>所以专家层，不再继续TP切分，即便你在vllm里面启用了TP切分</strong>。</p><p><img src="/images/Pasted%20image%2020260831205919.png"></p><p><mark style="background:#affad1">那vllm里面具体是如何设计EP来处理MoE架构的并行的呢?</mark></p><p><img src="/images/Pasted%20image%2020260831210000.png"></p><ul><li><mark style="background:#fff88f">分发：dispatch</mark><ul><li>每个EP rank, 除了各自负责的专家，还持有相同的ROUTER， 每个EP rank收到token 向量（这里每个EP rank收到的是不同的输入token。 也就是说，Router是复制的，但是输入token不一定相同）后，会计算出一个统一的结果（每个EP rank都计算出相同的处理分发结果：token向量+专家选择+专家打分权重），然后做一次All-to-All, 之后，每个EP rank就获得了各自需要计算的token向量+权重打分</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260831210805.png" alt="Pasted image 20260831210805.png" width="227" loading="lazy"><img src="/images/Pasted%20image%2020260831210822.png" alt="Pasted image 20260831210822.png" width="141" loading="lazy"><img src="/images/Pasted%20image%2020260831210844.png" alt="Pasted image 20260831210844.png" width="320" loading="lazy"></p><p><strong>下面看一下All-to-All</strong></p><img src="/images/Pasted%20image%2020260831210919.png" alt="Pasted image 20260831210919.png" width="392" loading="lazy"><img src="/images/Pasted%20image%2020260831211016.png" alt="Pasted image 20260831211016.png" width="412" loading="lazy"><hr><p>总结一下，EP + MoE下，每个EP rank的工作流程：<br><img src="/images/Pasted%20image%2020260831211824.png"></p><p><img src="/images/Pasted%20image%2020260831211832.png"></p><h4 id="Qwen3-30B-A3B的moe模型参数预览"><a href="#Qwen3-30B-A3B的moe模型参数预览" class="headerlink" title="Qwen3-30B-A3B的moe模型参数预览"></a>Qwen3-30B-A3B的moe模型参数预览</h4><p><img src="/images/Pasted%20image%2020260831212540.png"></p><p><mark style="background:#fff88f">hidden_states 是 2048维度，但是每个专家的up向量是仅有768维度</mark>。</p><img src="/images/Pasted%20image%2020260831212653.png" alt="Pasted image 20260831212653.png" width="573" loading="lazy"><img src="/images/Pasted%20image%2020260831212741.png" alt="Pasted image 20260831212741.png" width="526" loading="lazy"><p><img src="/images/Pasted%20image%2020260831212757.png"></p><hr><p><mark style="background:#ff4d4f">当Moe架构的模型，开启EP后，有这样几个注意点</mark>：</p><ul><li>EP &#x3D; TP x DP</li><li>EP 启用后，MoE不分，不再支持TP切分，也不支持DP复制独立模型副本，整个模型的MoE部分，仅仅是公有的一块独立权重。<ul><li>前面的注意力部分，仍然按照DP，TP 划分。</li><li>所以当DP &#x3D; 2， TP &#x3D; 4， 所以EP &#x3D; 8的时候<ul><li>每个rank持有，DP副本内的除开MoE部分的TP切分的一块权重，加上自己负责的MoE专家部分。</li></ul></li></ul></li></ul><img src="/images/Pasted%20image%2020260831214413.png" alt="Pasted image 20260831214413.png" width="632" loading="lazy"><h4 id="DP-2，TP-4，-EP使能的情况下，每个rank的工作流程"><a href="#DP-2，TP-4，-EP使能的情况下，每个rank的工作流程" class="headerlink" title="DP&#x3D;2，TP&#x3D;4， EP使能的情况下，每个rank的工作流程"></a>DP&#x3D;2，TP&#x3D;4， EP使能的情况下，每个rank的工作流程</h4><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">分组如下：</span><br><span class="line">  TP0    TP1     TP2    TP3</span><br><span class="line">DP0: rank0, rank1, rank2, rank3</span><br><span class="line">DP1: rank4, rank5, rank6, rank7</span><br><span class="line"></span><br><span class="line">所以，一个batch（batch0 + batch1）过来，</span><br><span class="line">DP0里面的4个rank，输入共同的batch0</span><br><span class="line">DP1里面的4个rank, 输入共同的batch1</span><br><span class="line"></span><br><span class="line">rank0,rank1,rank2,rank3, 输入相同，计算出各自切分的结果（列切分+行切分），然后tp组内all-reduce</span><br><span class="line">rank0,rank1,rank2,rank3 拥有相同的注意力部分的完整输出local_tokens（属于batch0）。</span><br><span class="line">同理，</span><br><span class="line">rank4, rank5, rank6, rank7 拥有相同的注意力部分的完整输出 local_tokens (属于batch1)</span><br><span class="line"></span><br><span class="line">此时开始，8张卡做各自的Router,</span><br><span class="line">（注意，因为rank 0,1,2,3都是同样的local_tokens，所以经过router后，也是同样的路由token，所以没必要都算router，而是选择让tp组内的各个rank，各自负责算一部分，然后dispatch各自负责的部分）</span><br><span class="line"></span><br><span class="line">然后all-to-all,</span><br><span class="line"></span><br><span class="line">每张rank获得了各自专家的hidden_states. 各自进行expert()前向。</span><br><span class="line"></span><br><span class="line">再次all-to-all, 拿回各rank原本的local_tokens中自己负责的专家路由的输出。进行专家权重加权求和。</span><br><span class="line"></span><br><span class="line">&gt;&gt;&gt; 到此时，一个tp组内的各个rank，持有自己负责路由token的专家加权输出（tp组内，每个rank持有的是token维度的切片）。</span><br><span class="line"></span><br><span class="line">所以接下来还需要一个all-gather的拼接操作，</span><br><span class="line">DP0的4个rank,拼出batch0的完整输出</span><br><span class="line">DP1的4个rank,拼出batch1的完整输出</span><br><span class="line">&gt;&gt;&gt; </span><br><span class="line"></span><br></pre></td></tr></table></figure><ol start="0"><li>当前layer开始计算。</li><li>每个rank，获取当前DP副本的一部分batch输入（组内4个tp rank处理同样的输入）</li><li>每个rank，使用自己持有的tp切分后的权重，输入tp组内同样的输入。 </li><li>每个rank， 执行一次all-reduce, tp组内，每个rank，都有了同样的完整的输出。</li><li>每个rank, 把注意力的输出（local_token），独立根据自己持有的部分专家路由（<mark style="background:#fff88f">tp组内的各个rank，各自负责算一部分</mark>），进行计算</li><li>所有的rank，进行<mark style="background:#fff88f">dispatch all-to-all</mark>, 然后expert前向。</li><li>所有的rank, 进行<mark style="background:#fff88f">combine all-to-all</mark>, 然后<mark style="background:#d3f8b6">每个 rank 会拿回“原本由自己负责的那些 token”的 expert 输出</mark>, 计算加权。输出结果。(此时tp组内，每个rank持有的是token维度的切片)</li><li>tp组内的rank，进行all-gather的拼接操作，拼出batch0的完整输出<ol><li><img src="/images/Pasted%20image%2020260831221430.png" alt="Pasted image 20260831221430.png" width="271" loading="lazy"></li></ol></li><li>开始下一层layer。</li></ol><h4 id="all-to-all通信"><a href="#all-to-all通信" class="headerlink" title="all-to-all通信"></a>all-to-all通信</h4><p>下面理解几个通信集合原语</p><h5 id="Scatter-ReduceScatter"><a href="#Scatter-ReduceScatter" class="headerlink" title="Scatter &#x2F;  ReduceScatter"></a>Scatter &#x2F;  ReduceScatter</h5><p>首先说明一下<br><mark style="background:#b1ffff">1. all-to-all:</mark></p><p><strong>all-to-all是一个独立的通信集合</strong>：<mark style="background:#d3f8b6">每个rank都会向所有rank发送一份数据，同时，也从所有rank接受数据</mark></p><p><mark style="background:#b1ffff">2. Scatter</mark> 分发<br>这个可以看做是Gather方向相反的通信，他需要在<strong>源rank上，准备一个scatter_list</strong>,  表示要发给各个目标rank的数据。</p><p>接收端，只需要准备目标张量即可</p><p>scatter_list的长度，必须和world_size一致，第i个元素张量，会发给local rank i</p><p>而<mark style="background:#fdbfff">ReduceScatter</mark>, <mark style="background:#fdbfff">AllReduce</mark>， 都会对所有rank上对应位置，做某种规约操作，比如求和，但是不同的是：</p><ul><li>AllReduce里面，每个rank都会得到完整的规约结果</li><li>ReduceScatter里面，规约完成后，还会按切片分发，每个rank只拿到结果的一部分</li></ul><p><img src="/images/Pasted%20image%2020260831223127.png"></p><img src="/images/Pasted%20image%2020260831223431.png" alt="Pasted image 20260831223431.png" width="665" loading="lazy"><h5 id="ReduceScatter"><a href="#ReduceScatter" class="headerlink" title="ReduceScatter"></a>ReduceScatter</h5><p>我们前面讲了Scatter，分发，这种通信集合方式。</p><p>现在开始ReduceScatter, 这个其实就是先看成Reduce, 在做Scatter分发。</p><ul><li>Reduce<ul><li>各rank同一位置分片累加</li></ul></li><li>Scatter<ul><li>按分片分发给不同rank</li></ul></li></ul><p>以上说的是接口语义，而底层实现，有很多种方式，比较常见的是ring实现，但ring并非唯一实现方案</p><img src="/images/Pasted%20image%2020260831224039.png" alt="Pasted image 20260831224039.png" width="479" loading="lazy"><img src="/images/Pasted%20image%2020260831224049.png" alt="Pasted image 20260831224049.png" width="481" loading="lazy"><p>三次更新之后，每块 <code>GPU</code> 上都有一块数据拥有了对应位置完整的累加聚合（上图中红色块）</p><blockquote><p>至此，是3次Reduce, 此时，<code>ReduceScatter</code> 通信阶段结束<br>$$Rank;i$$最终收到了所有节点上第 i 个数据块的求和结果。例如，Rank 0 汇总了所有节点的第一个 chunk（切片&#x2F;部分）块张量求和结果，更多节点也是依此类推</p></blockquote><h5 id="all-to-all"><a href="#all-to-all" class="headerlink" title="all-to-all"></a>all-to-all</h5><p><img src="/images/Pasted%20image%2020260831224427.png"></p><p><img src="/images/Pasted%20image%2020260831224511.png"></p><p><img src="/images/Pasted%20image%2020260831224602.png"></p><p><img src="/images/Pasted%20image%2020260831224609.png"><img src="/images/Pasted%20image%2020260831224619.png"></p><h2 id="投机解码"><a href="#投机解码" class="headerlink" title="投机解码"></a>投机解码</h2><p>投机解码还是很好理解的。直接看图<br>sits 是当前的草稿模型输入，产生的on my knee是草稿token</p><p>之后，大模型的输入就是4个（b + draft_tokens） &#x3D; sits + on my knee, 然后并行计算这4个token的验证输出</p><p>在经过多层大模型后，大模型的因果计算，就是prefill，得到：<br>sits : on（草稿on预测对了）<br>on : a （草稿my预测错了）(a作为最后的产出)<br>my : xx<br>knee : xx</p><p>所以，整体来看，对整个系统，你输入一个sits, 然后额外接受了草稿token: on, 然后得到输出token id 是a</p><blockquote><p>因此整体感受来说，投机解码，<mark style="background:#b1ffff">就是输入一个token, 输出下一个token</mark>， <mark style="background:#b1ffff">中间再额外选择几个草稿token</mark>.</p></blockquote><p>因此，<mark style="background:#fff88f">一次前向，输出的token长度是 &#x3D; 接受的草稿token + 最后一个草稿token预测出来的token</mark>.</p><p><img src="/images/Pasted%20image%2020260901201758.png"></p><p><img src="/images/Pasted%20image%2020260901203830.png"></p><p><img src="/images/Pasted%20image%2020260901203957.png"></p><h3 id="性能分析"><a href="#性能分析" class="headerlink" title="性能分析"></a>性能分析</h3><img src="/images/Pasted%20image%2020260901204147.png" alt="Pasted image 20260901204147.png" width="403" loading="lazy"><h3 id="Medusa"><a href="#Medusa" class="headerlink" title="Medusa"></a>Medusa</h3><p>Medusa是无草稿小模型 的 多头并行解码</p><img src="/images/Pasted%20image%2020260901204251.png" alt="Pasted image 20260901204251.png" width="533" loading="lazy"><p><img src="/images/Pasted%20image%2020260901204318.png"></p><blockquote><p>简单说就是，在大模型的最后一个layer后，lm_head之前，加入一个层（多个medusa头，每个头预测未来的一步），这个Medusa层，根据输出token(也就是后面的输入)，<mark style="background:#fff88f">多预测几个draft token</mark></p></blockquote><img src="/images/Pasted%20image%2020260901204542.png" alt="Pasted image 20260901204542.png" width="628" loading="lazy"><blockquote><p>所以medusa的结构，相比于普通的草稿模型，用更高效的预测结构，一次给出多步候选，并统一验证</p></blockquote><h4 id="模型架构"><a href="#模型架构" class="headerlink" title="模型架构"></a>模型架构</h4><p>常规的 decoding 过程称为 Next-Token 预测，Medusa 这种多 token 并行解码称为 <mark style="background:#fff88f">Next-Next-Tokens 预测</mark></p><p><mark style="background:#fff88f">其通过增加多个 <code>Medusa Head</code>，与原模型上的<code>LM Head</code>一同做预测</mark></p><h3 id="Eagle-投机解码"><a href="#Eagle-投机解码" class="headerlink" title="Eagle 投机解码"></a>Eagle 投机解码</h3><p>前面的叫做预测解码，这边，Eagle，翻译过来就是投机解码</p><p>Eagle (Extrapolation Algorithm for Greater Language-model Efficiency) 及其后续版本 Eagle-3 是目前投机解码（Speculative Decoding）领域中最先进（SOTA）的技术之一</p><p><img src="/images/Pasted%20image%2020260901212311.png"></p><p><mark style="background:#fff88f">Eagle 的结构非常简单</mark>：通常只是<strong>一层 Transformer Decoder Layer</strong>（加上一些 Projection 层）。它的参数量极小（通常 &lt; 1B，甚至几百 MB）。</p><p><img src="/images/Pasted%20image%2020260901213155.png"></p><p><img src="/images/Pasted%20image%2020260901213210.png"></p><h3 id="DFlash"><a href="#DFlash" class="headerlink" title="DFlash"></a>DFlash</h3><p><img src="/images/Pasted%20image%2020260901214108.png"></p><p>原本我们的草稿token的生成，是需要草稿模型的自回归的。</p><h3 id="DSpark"><a href="#DSpark" class="headerlink" title="DSpark"></a>DSpark</h3><h2 id="thank-you"><a href="#thank-you" class="headerlink" title="thank you"></a>thank you</h2>]]></content>
    
    
    <summary type="html">围绕 vLLM 的 AsyncLLM 与 EngineCore，记录 KV Cache、调度器以及 TP/PP/DP/EP、MoE 并行实现的 my-vLLM 学习笔记。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="推理系统" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E6%8E%A8%E7%90%86%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="KV Cache" scheme="https://liangji-seu.github.io/tags/KV-Cache/"/>
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="vLLM" scheme="https://liangji-seu.github.io/tags/vLLM/"/>
    
    <category term="TP" scheme="https://liangji-seu.github.io/tags/TP/"/>
    
    <category term="AsyncLLM" scheme="https://liangji-seu.github.io/tags/AsyncLLM/"/>
    
    <category term="EngineCore" scheme="https://liangji-seu.github.io/tags/EngineCore/"/>
    
    <category term="PP" scheme="https://liangji-seu.github.io/tags/PP/"/>
    
    <category term="DP" scheme="https://liangji-seu.github.io/tags/DP/"/>
    
    <category term="EP" scheme="https://liangji-seu.github.io/tags/EP/"/>
    
    <category term="专家并行" scheme="https://liangji-seu.github.io/tags/%E4%B8%93%E5%AE%B6%E5%B9%B6%E8%A1%8C/"/>
    
    <category term="MoE" scheme="https://liangji-seu.github.io/tags/MoE/"/>
    
  </entry>
  
  <entry>
    <title>infra Nsight Compute学习笔记</title>
    <link href="https://liangji-seu.github.io/2026/07/04/infra-Nsight%20Compute%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>https://liangji-seu.github.io/2026/07/04/infra-Nsight%20Compute%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
    <published>2026-07-04T01:21:52.000Z</published>
    <updated>2026-10-03T10:34:31.622Z</updated>
    
    <content type="html"><![CDATA[<p>这篇主要用来记录Nsight Compute的学习</p><h1 id="测试背景"><a href="#测试背景" class="headerlink" title="测试背景"></a>测试背景</h1><p>我主要使用的是矩阵乘法这个cuda kernel的不同优化版本来进行测试</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v1</span><span class="params">(<span class="type">int</span> M,<span class="comment">//A(M x K)   B(K x N)  C(M x N)</span></span></span></span><br><span class="line"><span class="params"><span class="function">   <span class="type">int</span> N,</span></span></span><br><span class="line"><span class="params"><span class="function">   <span class="type">int</span> K,</span></span></span><br><span class="line"><span class="params"><span class="function">   <span class="type">float</span> alpha,<span class="comment">//放缩系数</span></span></span></span><br><span class="line"><span class="params"><span class="function">   <span class="type">float</span> *A,</span></span></span><br><span class="line"><span class="params"><span class="function">                           <span class="type">float</span>* B,</span></span></span><br><span class="line"><span class="params"><span class="function">                           <span class="type">float</span> beta,<span class="comment">//放缩系数</span></span></span></span><br><span class="line"><span class="params"><span class="function">                           <span class="type">float</span> *C)</span></span></span><br></pre></td></tr></table></figure><p>不同版本kernel核函数对比如下：</p><ul><li>v1 <strong>朴素实现</strong><ul><li>每个thread，负责C的一个元素</li></ul></li><li>v2 <strong>共享内存优化</strong><ul><li>一个block，负责一个C_sub（搬运A_sub, B_sub 到 该block的shared_mem）</li><li>一个thread负责C_sub里面的一个元素</li><li>共享内存保存A_tile, B_tile</li><li>寄存器保存每个A_tile，B_tile的计算结果</li></ul></li><li>v3 <strong>thread_tile优化</strong><ul><li>基于v2</li><li>一个thread负责C_sub里多个元素 &#x3D; C_tile</li><li>共享内存保存A_tile, B_tile</li><li>寄存器保存每个A_tile，B_tile的计算结果</li></ul></li><li>v4 <strong>向量预取指令</strong><ul><li>基于v3</li><li>写共享内存As直接写<strong>转置</strong>，<strong>读共享内存</strong>时，优化<strong>读取的效率</strong></li></ul></li><li>v5 <strong>双缓冲+指令流水线重叠优化</strong></li><li>v6 <strong>warp tiling优化</strong></li></ul><p>benchmard的测试条件为：</p><ul><li>M&#x3D;2048</li><li>K&#x3D;2048</li><li>N&#x3D;2048</li></ul><p>测试结果为<br><img src="/images/Pasted%20image%2020260702192335.png" alt="366"></p><h1 id="Nsight-compute报告分析-以v1朴素实现的分析结果为例"><a href="#Nsight-compute报告分析-以v1朴素实现的分析结果为例" class="headerlink" title="Nsight compute报告分析(以v1朴素实现的分析结果为例)"></a>Nsight compute报告分析(以v1朴素实现的分析结果为例)</h1><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><blockquote><p>吞吐量：单位时间内统计数量：<br><strong>计算吞吐量</strong> compute throughput: <strong>单位时间内完成多少次计算</strong><br>SM 所有运算单元（FMA&#x2F;ALU&#x2F;FP64&#x2F;Tensor 等）<strong>完成的运算操作总次数</strong><br><strong>内存吞吐量</strong> memory throughput: <strong>单位时间传输的数据字节总量</strong></p></blockquote><p>关于各种计算硬件单元，GPU里面有很多负责计算单元：</p><ul><li><strong>LSU</strong> 加载 &#x2F; 存储访存单元</li><li><strong>FMA</strong> 浮点数乘加单元</li><li><strong>ALU</strong> 整数运算单元</li></ul><p><strong>每个硬件单元，都有自己独立的指令流水线</strong>，每条流水线只处理对应类型指令<br><img src="/images/Pasted%20image%2020260702195620.png" alt="332"></p><hr><h2 id="GPU-Speed-Of-Light-Throughput"><a href="#GPU-Speed-Of-Light-Throughput" class="headerlink" title="GPU Speed Of Light Throughput"></a>GPU Speed Of Light Throughput</h2><p>GPU 理论峰值吞吐量分析面板</p><p>本面板整体<strong>展示 GPU 计算单元</strong>与显存资源的吞吐量概况。<br>每项硬件单元指标会输出<strong>当前实际吞吐量</strong>相对理论峰值的利用率百分比</p><p><img src="/images/Pasted%20image%2020260702195840.png"><img src="/images/Pasted%20image%2020260702195857.png" alt="319"><img src="/images/Pasted%20image%2020260702195909.png" alt="365"></p><p>可以看到SM的计算单元指令吞吐量占满了，<br>显存访问吞吐量也占满了，其中主要是一级缓存在疯狂使用。</p><blockquote><p><strong>计算吞吐量</strong>只统计 FMA&#x2F;ALU 等运算操作，和 LSU 访存无关</p><p>这里 98.29% 代表：<strong>只要 SM 处于活跃、没有阻塞等待的周期里，运算单元几乎跑满了理论算力上限</strong>。</p></blockquote><p><img src="/images/Pasted%20image%2020260702200024.png">1. 高吞吐量提示（High Throughput）</p><p>当前内核程序<strong>占用设备 80% 以上的计算或显存峰值性能</strong> （<mark style="background:#fff88f">计算能力压榨的好像还不错</mark>）。若需进一步提升性能，建议将计算负载从当前高占用硬件单元迁移至其他模块。</p><p>可先前往「计算负载分析」板块分析程序负载特征。</p><p><img src="/images/Pasted%20image%2020260702200118.png">2. 屋顶线分析提示（Roofline Analysis）</p><p>本 GPU 设备单精度浮点（FP32）与双精度浮点（FP64）理论峰值性能比值为 64:1。</p><p>当前内核程序<strong>仅达到设备单精度峰值性能的 6%</strong>，双精度峰值性能利用率为 0%。<br>说明<mark style="background:#ff4d4f">计算能力仅到达峰值的6%</mark>， 差的很多</p><h2 id="Compute-Workload-Analysis"><a href="#Compute-Workload-Analysis" class="headerlink" title="Compute Workload Analysis"></a>Compute Workload Analysis</h2><p>计算负载分析<br>本模块<strong>对流式多处理器（SM）的计算资源</strong>进行精细化分析，包含实际每周期执行指令数（IPC）与各类流水线的<strong>硬件利用率</strong>。流水线利用率过高时，会成为整体性能瓶颈<br><img src="/images/Pasted%20image%2020260702200321.png"><br><img src="/images/Pasted%20image%2020260702200406.png" alt="320"><img src="/images/Pasted%20image%2020260702200417.png" alt="285"></p><p><img src="/images/Pasted%20image%2020260702200435.png">⚠️ 硬件利用率偏低<br>所有<mark style="background:#ff4d4f">计算流水线均未充分利用</mark>。出现该现象有两种可能性：当前 GPU 内核计算量过小；或是调度器未生成足够多的线程束（warp）。</p><p><img src="/images/Pasted%20image%2020260702200509.png" alt="534"></p><p>流水线利用率（占 SM 活跃周期百分比）<br><img src="/images/Pasted%20image%2020260702200541.png" alt="319"></p><p>这个是反应的计算指令的类别占比，可以看出我们程序主要是进行什么计算，这里可以看出，主要是FMA计算。<br><img src="/images/Pasted%20image%2020260702200647.png" alt="497"></p><p><img src="/images/Pasted%20image%2020260702200735.png">流水线利用率（占硬件峰值可执行指令总量百分比）</p><p>这个主要反应的是，一段绝对时间内，SM主要在进行什么指令，这里可以看出，大部分实际都是LSU，也就是访存</p><p>所有线程执行流程都被内存读写指令阻塞，<strong>硬件绝大多数周期用于加载、存储数据</strong>，没有空余周期执行浮点 &#x2F; 整数计算。</p><p><strong>所以，总结下来：V1 的 matmul的朴素实现，是一个</strong></p><p><strong>访存密集型</strong>：海量全局内存读写操作；<br><strong>仅基础单精度浮点 + 少量整数运算</strong>，无高精度、AI 张量、纹理采样、复杂分支逻辑；</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><blockquote><p><strong>计算吞吐量</strong> 是看100个时间点里面，最忙的子管道（指令）工作了多少时间</p><p>这里 98.29% 代表：<strong>只要 SM 处于活跃、没有阻塞等待的周期里，运算单元几乎跑满了理论算力上限</strong>。我们可以查看breakdown细则，发现，最忙的是LSU指令。<br><img src="/images/Pasted%20image%2020260702202650.png" alt="362"></p></blockquote><blockquote><p>结合前面流水线图表：<strong>活跃周期内 FMA 流水线占比 22%、ALU 占 7%</strong>，在仅有的少量能干活的时间里，算力硬件被充分利用。这里的占比，是指的FMA在他的所有时间内，只工作了22个时间<br><img src="/images/Pasted%20image%2020260702202800.png" alt="489"></p></blockquote><p>所以现在看这个SM的峰值利用率，可以看到在SM的活跃时期100个时间，LSU一直都在跑，<br><img src="/images/Pasted%20image%2020260702203833.png"><br><img src="/images/Pasted%20image%2020260702202946.png"></p><p><img src="/images/Pasted%20image%2020260702203825.png"><br><img src="/images/Pasted%20image%2020260702203846.png" alt="620"></p><p><img src="/images/Pasted%20image%2020260702203913.png" alt="556"><br><img src="/images/Pasted%20image%2020260702203932.png" alt="527"></p><h2 id="Memory-Workload-Analysis"><a href="#Memory-Workload-Analysis" class="headerlink" title="Memory Workload Analysis"></a>Memory Workload Analysis</h2><p><img src="/images/Pasted%20image%2020260702204009.png"></p><p>这里就是分析GPU的显存资源，</p><ul><li><strong>内存吞吐量</strong> 20.41 Gb&#x2F;s <mark style="background:#ff4d4f">低</mark><ul><li><img src="/images/Pasted%20image%2020260702204525.png" alt="319"></li></ul></li><li><strong>L1缓存命中率</strong> 94.96% <mark style="background:#ff4d4f">高</mark><ul><li><img src="/images/Pasted%20image%2020260702204551.png" alt="279"></li></ul></li><li>L2缓存命中率 97.73% <mark style="background:#ff4d4f">高</mark><ul><li><img src="/images/Pasted%20image%2020260702204649.png" alt="233"></li></ul></li><li>L2压缩成功率 0%<ul><li><img src="/images/Pasted%20image%2020260702210901.png" alt="352"></li></ul></li><li>内存繁忙率 49%  <strong>整体 memory 系统忙碌程度</strong></li><li>最大带宽占满率 98.29% <mark style="background:#ff4d4f">极个别管线最忙</mark>  最忙 memory 子资源的带宽利用程度</li><li>内存管道繁忙率 98.29%  <strong>memory 指令管线忙碌程度</strong><ul><li><img src="/images/Pasted%20image%2020260702204727.png" alt="322"></li><li><img src="/images/Pasted%20image%2020260702204802.png" alt="452"></li></ul></li><li><h2 id="L2压缩率-0"><a href="#L2压缩率-0" class="headerlink" title="L2压缩率 0%"></a>L2压缩率 0%</h2><ul><li><img src="/images/Pasted%20image%2020260702210929.png" alt="296"></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260702205034.png"></p><p><mark style="background:#ff4d4f">黄色警告提示：L2 Load Access Pattern</mark><br>从 <strong>L1 纹理缓存向 L2 缓存发起的读取访存模式未达到最优</strong>。</p><p>L1 纹理缓存向 L2 发起一次请求的最小粒度为一条 128 字节缓存行，一条缓存行包含 4 段连续 32 字节扇区。</p><p>但当前内核每次读取缓存行时，平均仅访问 4 个扇区中的 1.6 个，存在大量无效缓存行加载开销。</p><p>前往「源码计数器」板块查看<strong>未合并离散读取</strong>相关指标，<strong>优化思路：尽可能减少单次内存请求需要加载的缓存行数量</strong>。<br><img src="/images/Pasted%20image%2020260702205858.png"></p><p>接下来是分析<br><img src="/images/Pasted%20image%2020260702211116.png" alt="688"><br><img src="/images/Pasted%20image%2020260702211129.png"></p><p>这个 <strong>Memory Chart</strong> 是把刚才表格里的 memory workload 画成一张“数据流图”</p><blockquote><p>你的 v1 naive SGEMM <strong>几乎只使用 global memory 路径</strong>，<br><strong>没有 shared memory</strong>；</p><p>大量 global load&#x2F;store 请求进入 L1&#x2F;TEX，L1&#x2F;L2 命中率很高，真正到 DRAM 的数据不多，但 global→L1TEX 以及 L1TEX→L2 的请求模式存在 warning。</p></blockquote><p><img src="/images/Pasted%20image%2020260702211420.png" alt="249"><img src="/images/Pasted%20image%2020260702212054.png" alt="178"><img src="/images/Pasted%20image%2020260702212119.png" alt="257"></p><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p>所以，根据以上分析，可以看出几个问题</p><ul><li><mark style="background:#ff4d4f">显存带宽低</mark>，说明显存带宽<strong>不是瓶颈</strong></li><li>L2-&gt;L1的访存请求粒度利用不充分<ul><li>每次访存请求没有充分利用完整 cache line，<strong>访问的L2的cache line的利用率不够</strong>。</li></ul></li></ul><h2 id="scheduler-statistics"><a href="#scheduler-statistics" class="headerlink" title="scheduler statistics"></a>scheduler statistics</h2><p><img src="/images/Pasted%20image%2020260702212810.png"></p><p>warp 调度器有没有足够多的 warp 可以发射指令。</p><p>SM 里的 warp scheduler 每个周期有没有活干？能不能连续发指令？</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">GPU Maximum Warps <span class="type">Per</span> <span class="variable">Scheduler</span>       <span class="operator">=</span> <span class="number">12</span>   <span class="comment">//硬件上限，每个 warp scheduler 理论最多能管理 12 个 warp。</span></span><br><span class="line">Theoretical Warps <span class="type">Per</span> <span class="variable">Scheduler</span>       <span class="operator">=</span> <span class="number">8</span>    <span class="comment">//根据你的 kernel 配置算出来的理论 occupancy</span></span><br><span class="line"><span class="comment">//一个 SM 通常有 4 个 warp scheduler，所以平均到每个 scheduler</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Active Warps <span class="type">Per</span> <span class="variable">Scheduler</span>            <span class="operator">=</span> <span class="number">7.93</span></span><br><span class="line"><span class="comment">//每个 scheduler 平均有 7.93 个 active warp。它非常接近 theoretical 的 8，说明你的 kernel 基本达到了由 launch config 限制下的理论 occupancy。</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Eligible Warps <span class="type">Per</span> <span class="variable">Scheduler</span>          <span class="operator">=</span> <span class="number">1.10</span>  <span class="comment">//每个调度器的就绪 warp，当前已经准备好，可以发出下一条指令</span></span><br><span class="line"><span class="comment">//大部分 warp 虽然在 SM 上，但都卡住了，说明大部分的warp都在阻塞中，等待访存返回</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Issued Warp <span class="type">Per</span> <span class="variable">Scheduler</span>             <span class="operator">=</span> <span class="number">0.27</span> <span class="comment">//每个 scheduler 每个 cycle 平均真正发出了多少个 warp 指令</span></span><br><span class="line"><span class="comment">//理想值 ≈ 1.0， 这说明 issue slot 利用率很低，调度器经常空转。</span></span><br><span class="line"></span><br><span class="line"><span class="type">No</span> <span class="variable">Eligible</span>                           <span class="operator">=</span> <span class="number">72.94</span>% </span><br><span class="line"><span class="comment">// 有 72.94% 的周期，scheduler 找不到任何一个可以发指令的 warp。</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">One or <span class="type">More</span> <span class="variable">Eligible</span>                  <span class="operator">=</span> <span class="number">27.06</span>%</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260702213633.png"></p><h2 id="Warp-state-statistics"><a href="#Warp-state-statistics" class="headerlink" title="Warp state statistics"></a>Warp state statistics</h2><p>warp 在两个连续指令之间，到底把时间花在了哪里</p><p>前面的 <strong>Scheduler Statistics</strong> 告诉你：“调度器经常发不出指令”。<br>这个 <strong>Warp State Statistics</strong> 进一步告诉你：“为什么 warp 发不出指令”</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Warp Cycles Per <span class="type">Issued</span> <span class="variable">Instruction</span> <span class="operator">=</span> <span class="number">29.31</span>  <span class="comment">//平均每个 warp 发出一条指令，要隔大约 29.31 个 cycle。</span></span><br><span class="line"><span class="comment">//理想情况下，warp 应该比较频繁地发指令。你这里 29.31 cycle 才发一条，说明 warp 大部分时间都在 stall。</span></span><br><span class="line"><span class="comment">//对应：No Eligible = 72.94%</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Warp Cycles Per <span class="type">Executed</span> <span class="variable">Instruction</span> <span class="operator">=</span> <span class="number">29.31</span>   </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Avg. Active Threads <span class="type">Per</span> <span class="variable">Warp</span> <span class="operator">=</span> <span class="number">32</span>   <span class="comment">//每个 warp 基本都是 32 个线程全活跃</span></span><br><span class="line"><span class="comment">//你这个 kernel 不是因为分支发散导致性能差。</span></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">Avg. Not Predicated Off Threads/Warp = <span class="number">31.98</span>  <span class="comment">//绝大多数线程没有被谓词屏蔽，基本都在执行有效指令。</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260702214309.png"><br>每个 warp <strong>平均有 20 个周期卡住</strong>，是因为 local&#x2F;global <strong>memory 指令要进入 L1TEX&#x2F;LSU 相关队列</strong>，但是这个<strong>队列满了</strong>，新的访存指令发不进去。</p><p><img src="/images/Pasted%20image%2020260702214404.png" alt="331"></p><p>warp的其他几个项：</p><ul><li>Stall Not Selected<ul><li>这个 warp 已经 ready 了，但是调度器这一拍没有选它。</li></ul></li><li>Stall Wait<ul><li><img src="/images/Pasted%20image%2020260702214506.png" alt="246"></li></ul></li><li>Stall Long Scoreboard<ul><li>warp 在等一个<strong>较长延迟的数据依赖</strong>，常见是 global memory &#x2F; L2 &#x2F; DRAM load 的返回。</li></ul></li><li>Stall Dispatch Stall<ul><li>这个和指令分发&#x2F;执行资源有关，表示 warp 虽然有指令，但由于 dispatch 相关限制没能顺利发出去</li></ul></li><li>Selected<ul><li>warp 被 scheduler 选中并成功发出指令的周期。</li></ul></li></ul><h2 id="Launch-Statistics"><a href="#Launch-Statistics" class="headerlink" title="Launch Statistics"></a>Launch Statistics</h2><p>kernel 是怎么启动的，以及这个启动配置会消耗多少 GPU 资源。</p><figure class="highlight fortran"><table><tr><td class="code"><pre><span class="line">Grid <span class="built_in">Size</span>                         = <span class="number">4096</span></span><br><span class="line"><span class="keyword">Block</span> <span class="built_in">Size</span>                        = <span class="number">1024</span></span><br><span class="line">Threads                           = <span class="number">4</span>,<span class="number">194</span>,<span class="number">304</span></span><br><span class="line">Registers Per Thread              = <span class="number">40</span></span><br><span class="line">//一个寄存器 <span class="number">4</span>字节， 每个thread占用<span class="number">40</span>*<span class="number">4</span> = <span class="number">160</span>字节的寄存器资源</span><br><span class="line"></span><br><span class="line">//共享内存</span><br><span class="line">Static <span class="keyword">Shared</span> Memory Per <span class="keyword">Block</span>    = <span class="number">0</span></span><br><span class="line">Dynamic <span class="keyword">Shared</span> Memory Per <span class="keyword">Block</span>   = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">//这个不是我们主动申请的共享内存</span><br><span class="line">//CUDA driver / runtime / profiling / kernel 启动环境可能额外占用的一点 <span class="keyword">shared</span> memory 资源。</span><br><span class="line">Driver <span class="keyword">Shared</span> Memory Per <span class="keyword">Block</span>    = <span class="number">1.02</span> KB</span><br><span class="line"></span><br><span class="line">//表示当前 <span class="keyword">shared</span> memory 配置/分配粒度相关的容量显示</span><br><span class="line"><span class="keyword">Shared</span> Memory Configuration <span class="built_in">Size</span>  = <span class="number">8.19</span> KB</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">//这个表示你没有显式设置 L1/<span class="keyword">shared</span> memory 偏好。</span><br><span class="line"><span class="function"><span class="keyword">Function</span></span> Cache Configuration      = CachePreferNone</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">//所有 <span class="keyword">block</span> 分配到 SM 上执行时，大约要分 <span class="number">32</span> 波跑完。</span><br><span class="line">Waves Per SM                      = <span class="number">32</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260702215523.png" alt="331"></p><p><mark style="background:#fff88f">如果扑不满会怎么样</mark>，我在编写kernel的时候，怎么会知道GPU的SM数量，每个SM的寄存器大小呢？</p><p><img src="/images/Pasted%20image%2020260702215655.png" alt="457"><br><img src="/images/Pasted%20image%2020260702215722.png" alt="523"></p><p><img src="/images/Pasted%20image%2020260702215800.png"><br><img src="/images/Pasted%20image%2020260702215808.png"></p><h3 id="写-kernel-时怎么知道-SM-数量？"><a href="#写-kernel-时怎么知道-SM-数量？" class="headerlink" title="写 kernel 时怎么知道 SM 数量？"></a><mark style="background:#fff88f"><strong>写 kernel 时怎么知道 SM 数量</strong>？</mark></h3><p>最标准方法是用 CUDA Runtime API 查询。</p><p><img src="/images/Pasted%20image%2020260702215903.png" alt="494"><img src="/images/Pasted%20image%2020260702215914.png" alt="158"></p><h3 id="怎么知道自己的-kernel-每个线程用了多少寄存器"><a href="#怎么知道自己的-kernel-每个线程用了多少寄存器" class="headerlink" title="怎么知道自己的 kernel 每个线程用了多少寄存器"></a>怎么知道自己的 kernel 每个线程用了多少寄存器</h3><ol><li>Nsight Compute</li><li><img src="/images/Pasted%20image%2020260702220018.png" alt="275"></li><li><img src="/images/Pasted%20image%2020260702220032.png" alt="475"></li></ol><h3 id="怎么知道一个-SM-能同时放几个-block？"><a href="#怎么知道一个-SM-能同时放几个-block？" class="headerlink" title="怎么知道一个 SM 能同时放几个 block？"></a>怎么知道一个 SM 能同时放几个 block？</h3><p><img src="/images/Pasted%20image%2020260702220115.png" alt="332"></p><p><img src="/images/Pasted%20image%2020260702220157.png" alt="324"></p><p>这个函数会帮你根据 <strong>kernel 的寄存器</strong>、<strong>shared memory</strong>、<strong>block size</strong> 自动算出理论 <strong>occupancy</strong>。</p><h3 id="实战中写kernel的流程"><a href="#实战中写kernel的流程" class="headerlink" title="实战中写kernel的流程"></a>实战中写kernel的流程</h3><p><img src="/images/Pasted%20image%2020260702220238.png"></p><h2 id="Occupancy"><a href="#Occupancy" class="headerlink" title="Occupancy"></a>Occupancy</h2><p>一个 SM 上到底同时驻留了多少 warp，占硬件最大可驻留 warp 数的比例</p><p><img src="/images/Pasted%20image%2020260702220450.png" alt="314"></p><p><img src="/images/Pasted%20image%2020260702215234.png" alt="427"></p><figure class="highlight mathematica"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="operator">//</span>一个 <span class="variable">SM</span> 最大 <span class="variable">active</span> <span class="variable">warps</span> 大约是<span class="number">48</span> <span class="variable">warps</span> <span class="operator">/</span> <span class="variable">SM</span> 在<span class="number">4090</span>上</span><br><span class="line"><span class="variable">Theoretical</span> <span class="variable">Occupancy</span>              <span class="operator">=</span> <span class="number">66.67</span><span class="operator">%</span></span><br><span class="line"><span class="variable">Theoretical</span> <span class="built_in">Active</span> <span class="variable">Warps</span> <span class="variable">per</span> <span class="variable">SM</span>    <span class="operator">=</span> <span class="number">32</span></span><br><span class="line"><span class="variable">Achieved</span> <span class="variable">Occupancy</span>                 <span class="operator">=</span> <span class="number">66.01</span><span class="operator">%</span></span><br><span class="line"><span class="variable">Achieved</span> <span class="built_in">Active</span> <span class="variable">Warps</span> <span class="variable">per</span> <span class="variable">SM</span>       <span class="operator">=</span> <span class="number">31.68</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">Block</span> <span class="built_in">Limit</span> <span class="variable">Registers</span>              <span class="operator">=</span> <span class="number">1</span></span><br><span class="line"><span class="built_in">Block</span> <span class="built_in">Limit</span> <span class="variable">Shared</span> <span class="variable">Mem</span>             <span class="operator">=</span> <span class="number">8</span></span><br><span class="line"><span class="built_in">Block</span> <span class="built_in">Limit</span> <span class="variable">Warps</span>                  <span class="operator">=</span> <span class="number">1</span></span><br><span class="line"><span class="built_in">Block</span> <span class="built_in">Limit</span> <span class="variable">SM</span>                     <span class="operator">=</span> <span class="number">24</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260702220659.png" alt="177"><img src="/images/Pasted%20image%2020260702220715.png" alt="265"><img src="/images/Pasted%20image%2020260702220730.png" alt="363"></p><h3 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h3><p><img src="/images/Pasted%20image%2020260702220747.png" alt="529"></p><p><img src="/images/Pasted%20image%2020260702220821.png" alt="291"></p><h2 id="Source-Counters"><a href="#Source-Counters" class="headerlink" title="Source Counters"></a>Source Counters</h2><p>把前面看到的 <strong>stall、分支、指令数量</strong> 对应回源码行。</p><p><img src="/images/Pasted%20image%2020260702221056.png"></p><p><img src="/images/Pasted%20image%2020260702221143.png" alt="228"><img src="/images/Pasted%20image%2020260702221114.png" alt="259"><img src="/images/Pasted%20image%2020260702221128.png" alt="274"></p><p><img src="/images/Pasted%20image%2020260702221159.png" alt="355"><img src="/images/Pasted%20image%2020260702221213.png" alt="323"></p><h1 id="计算密集型-显存密集型-计算瓶颈-显存瓶颈"><a href="#计算密集型-显存密集型-计算瓶颈-显存瓶颈" class="headerlink" title="计算密集型 - 显存密集型 &#x2F; 计算瓶颈 - 显存瓶颈"></a>计算密集型 - 显存密集型 &#x2F; 计算瓶颈 - 显存瓶颈</h1><p><img src="/images/Pasted%20image%2020260702221906.png" alt="691"></p><p><strong>计算密集型</strong><br>每搬 1 byte 数据，可以做很多次计算。</p><p>核心指标：<br><strong>算术强度</strong> &#x3D; 浮点运算<strong>次数</strong> &#x2F; 访存<strong>字节数</strong></p><p><img src="/images/Pasted%20image%2020260702222043.png" alt="534"></p><p><strong>显存密集型</strong>：<br>每搬 1 byte 数据，只做很少计算<br><img src="/images/Pasted%20image%2020260702222125.png" alt="454"></p><p><mark style="background:#ff4d4f">计算瓶颈</mark><br>kernel 的<strong>速度主要受计算单元限制</strong>，比如 FMA &#x2F; Tensor Core &#x2F; SFU 等计算管线已经很忙，<strong>继续优化访存意义不大</strong>，应该优化计算组织。</p><p><img src="/images/Pasted%20image%2020260702222231.png" alt="568"></p><p><mark style="background:#ff4d4f">显存瓶颈</mark><br>显存瓶颈有<strong>两种</strong>，要分清</p><p><img src="/images/Pasted%20image%2020260702222355.png" alt="453"></p><p><img src="/images/Pasted%20image%2020260702222441.png" alt="484"></p><h2 id="如何准确判断"><a href="#如何准确判断" class="headerlink" title="如何准确判断"></a>如何准确判断</h2><p><img src="/images/Pasted%20image%2020260702222648.png" alt="549"></p><p><img src="/images/Pasted%20image%2020260702222804.png" alt="235"><img src="/images/Pasted%20image%2020260702222819.png" alt="206"></p><p><img src="/images/Pasted%20image%2020260702222902.png" alt="456"></p><p><img src="/images/Pasted%20image%2020260702222944.png"></p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">计算密集型算子：</span><br><span class="line">    特点：FLOPs<span class="symbol">/Byte</span> 高，每读入一点数据，可以做很多计算。</span><br><span class="line">    目标：让 FMA<span class="symbol">/Tensor</span> Core 尽量忙。</span><br><span class="line">    优化：</span><br><span class="line">        <span class="number">1</span>. shared memory tiling，提高 block 内复用</span><br><span class="line">        <span class="number">2</span>. register blocking，提高 thread 内复用</span><br><span class="line">        <span class="number">3</span>. 减少 global load</span><br><span class="line">        <span class="number">4</span>. 合理调度，让计算和访存重叠</span><br><span class="line">        <span class="number">5</span>. 提高 FMA<span class="symbol">/Tensor</span> Core 利用率</span><br><span class="line"></span><br><span class="line">显存密集型算子：</span><br><span class="line">    特点：FLOPs<span class="symbol">/Byte</span> 低，每搬很多数据，只做少量计算。</span><br><span class="line">    目标：让显存<span class="symbol">/L2/L1</span> 访问尽量高效。</span><br><span class="line">    优化：</span><br><span class="line">        <span class="number">1</span>. coalesced access</span><br><span class="line">        <span class="number">2</span>. vectorized load<span class="symbol">/store</span></span><br><span class="line">        <span class="number">3</span>. 减少无效访存</span><br><span class="line">        <span class="number">4</span>. 提高 cache line <span class="symbol">/</span> sector 利用率</span><br><span class="line">        <span class="number">5</span>. 尽量提高实际内存带宽</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">以矩阵乘法 CUDA kernel 为例，记录 Nsight Compute 各项性能指标、瓶颈判断与 kernel 优化分析。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="算子与性能" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E7%AE%97%E5%AD%90%E4%B8%8E%E6%80%A7%E8%83%BD/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="Nsight Compute" scheme="https://liangji-seu.github.io/tags/Nsight-Compute/"/>
    
  </entry>
  
  <entry>
    <title>infra 单元测试库GTest+glog</title>
    <link href="https://liangji-seu.github.io/2026/07/04/infra-%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E5%BA%93GTest+glog/"/>
    <id>https://liangji-seu.github.io/2026/07/04/infra-%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E5%BA%93GTest+glog/</id>
    <published>2026-07-04T01:21:52.000Z</published>
    <updated>2026-10-03T10:34:31.624Z</updated>
    
    <content type="html"><![CDATA[<p>这个文档主要用于记录学习如何使用GTest + glog来对我们一个系统里面各个模块的各个层类进行单元测试，现在发现如果手搓整个项目的话，不进行单元测试，比较麻烦。</p><ul><li>GTest:<ul><li>第三方库，用于测试程序，进行单元测试</li></ul></li><li>glog<ul><li>第三方库，用于打印调试信息log+断言检查CHECK</li></ul></li></ul><h1 id="GTest"><a href="#GTest" class="headerlink" title="GTest"></a>GTest</h1><p>这是一个第三方库。</p><h2 id="整个项目说明"><a href="#整个项目说明" class="headerlink" title="整个项目说明"></a>整个项目说明</h2><p>目前，我是基于kuipa的项目来学习的，所以，我先介绍一下，整个项目的CMakeLists来了解一下整个项目架构</p><p>kuipa项目结构如下</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">(base) liangji@ubun:~/AI_INFRA/projects/teacher/KuiperLLama$ l -l</span><br><span class="line">total 60</span><br><span class="line">drwxrwxr-x 5 liangji liangji 4096 Jun 28 11:29 build/      <span class="comment">#构建目录，我们需要在这个目录来构建makefile编译</span></span><br><span class="line"></span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun 27 17:13 cmake/            <span class="comment">#一些工具小脚本，检测cuda</span></span><br><span class="line">-rw-rw-r-- 1 liangji liangji 3521 Jun 28 11:11 CMakeLists.txt    <span class="comment">#整个项目的顶层构建脚本</span></span><br><span class="line"></span><br><span class="line">drwxrwxr-x 4 liangji liangji 4096 Jun  1 23:56 kuiper/           <span class="comment">#推理框架引擎层，最终编译出一个引擎.so</span></span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun 29 00:54 lib/              <span class="comment">#存放引擎.so</span></span><br><span class="line"></span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun  2 00:05 models/           <span class="comment">#存放外部模型权重文件，架构文件</span></span><br><span class="line"></span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun  2 00:09 demo/             <span class="comment">#主编译目标源文件，调用框架层，实现demo</span></span><br><span class="line"></span><br><span class="line">drwxrwxr-x 7 liangji liangji 4096 Jun  1 23:56 <span class="built_in">test</span>/             <span class="comment">#副编译目标源文件，框架层的单元测试</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 下面都是无关紧要的</span></span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun  1 23:56 hf_infer/</span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun  1 23:56 imgs/</span><br><span class="line">drwxrwxr-x 2 liangji liangji 4096 Jun  1 23:56 tmp/</span><br><span class="line">drwxrwxr-x 3 liangji liangji 4096 Jun  1 23:56 tools/</span><br><span class="line">-rw-rw-r-- 1 liangji liangji 2766 Jun  1 23:56 dockerfile</span><br><span class="line">-rw-rw-r-- 1 liangji liangji 5060 Jun  1 23:56 readme.md</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们的推理框架的主要代码在kuiper&#x2F;下，顶层的CMakeLists将这个kuiper&#x2F;下编译成推理框架llamalib.so</p><p>之后我们具体编译出可执行目标的是</p><ul><li>demo<ul><li>用来编译出真正可以运行的大模型推理的可执行程序</li></ul></li><li>test<ul><li>用来编译出<strong>针对单元测试</strong>的可执行程序</li></ul></li></ul><p>我们的GTest, glog，主要是针对test&#x2F;而言的，</p><p>在test&#x2F;下的CMakeLists里面，</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">include(../cmake/cuda.cmake)</span><br><span class="line">find_package(GTest REQUIRED)</span><br><span class="line">find_package(glog REQUIRED)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span>(link_ext_lib glog::glog GTest::gtest)</span><br><span class="line">aux_source_directory(../test DIR_TEST)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">aux_source_directory(../test/test_cu DIR_TEST_CU)</span><br><span class="line">aux_source_directory(../test/test_op DIR_TEST_OP)</span><br><span class="line">aux_source_directory(../test/test_model DIR_TEST_MODEL)</span><br><span class="line">aux_source_directory(../test/test_tensor DIR_TEST_TENSOR)</span><br><span class="line">aux_source_directory(../test/optimized DIR_TEST_OPTIMIZED)</span><br><span class="line"></span><br><span class="line">  </span><br><span class="line">add_executable(test_llm <span class="variable">$&#123;DIR_TEST&#125;</span> <span class="variable">$&#123;DIR_TEST_CU&#125;</span> <span class="variable">$&#123;DIR_TEST_OP&#125;</span> <span class="variable">$&#123;DIR_TEST_OPTIMIZED&#125;</span> <span class="variable">$&#123;DIR_TEST_TENSOR&#125;</span> <span class="variable">$&#123;DIR_TEST_MODEL&#125;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#set(CMAKE_CUDA_FLAGS &quot;$&#123;CMAKE_CUDA_FLAGS&#125; -g -G&quot;)</span></span><br><span class="line">target_link_libraries(test_llm <span class="variable">$&#123;link_ext_lib&#125;</span>)</span><br><span class="line">  </span><br><span class="line">  </span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定头文件：两个库的头文件 + 我们主程序的头文件</span></span><br><span class="line">target_include_directories(test_llm PUBLIC <span class="variable">$&#123;glog_INCLUDE_DIR&#125;</span>)</span><br><span class="line">target_include_directories(test_llm PUBLIC <span class="variable">$&#123;GTest_INCLUDE_DIR&#125;</span>)</span><br><span class="line">target_include_directories(test_llm PUBLIC ../kuiper/include)</span><br><span class="line"> </span><br><span class="line">  </span><br><span class="line"></span><br><span class="line">target_link_directories(test_llm PUBLIC <span class="variable">$&#123;PROJECT_SOURCE_DIR&#125;</span>/lib)</span><br><span class="line"><span class="keyword">if</span> (LLAMA3_SUPPORT OR QWEN2_SUPPORT OR QWEN3_SUPPORT)</span><br><span class="line">    message(STATUS <span class="string">&quot;LINK LLAMA3 SUPPORT&quot;</span>)</span><br><span class="line">    find_package(absl REQUIRED)</span><br><span class="line">    find_package(re2 REQUIRED)</span><br><span class="line">    find_package(nlohmann_json REQUIRED)</span><br><span class="line">    target_link_libraries(llama absl::base re2::re2 nlohmann_json::nlohmann_json)</span><br><span class="line">endif ()</span><br><span class="line"></span><br><span class="line">target_link_libraries(test_llm llama)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">set_target_properties(test_llm PROPERTIES WORKING_DIRECTORY <span class="variable">$&#123;CMAKE_SOURCE_DIR&#125;</span>)</span><br><span class="line">set_target_properties(test_llm PROPERTIES CUDA_SEPARABLE_COMPILATION ON)</span><br></pre></td></tr></table></figure><p>可以看到，我们的编译目标是test_llm ， 然后链接了GTest, glog这两个第三方库。</p><p>所以这个test_llm就是我们的单元测试的主程序。</p><p>单元测试的框架如下，主程序的main, 写在了test_main.cpp里面<br><img src="/images/Pasted%20image%2020260703131733.png" alt="157"></p><h2 id="主程序"><a href="#主程序" class="headerlink" title="主程序"></a>主程序</h2><p>下面看一下单元测试test_llm这个可执行文件的主程序main</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;glog/logging.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;gtest/gtest.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">//./test_llm --gtest_filter=test_buffer.*</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span>* argv[])</span> &#123;</span><br><span class="line"><span class="comment">//初始化 GTest 框架。 解析命令行参数（比如 `--gtest_filter`），去掉 GTest 认识的参数，把剩下的留给用户</span></span><br><span class="line">  testing::InitGoogleTest(&amp;argc, argv);</span><br><span class="line">  </span><br><span class="line"><span class="comment">//初始化 glog。 参数 `&quot;Kuiper&quot;` 是程序名，会出现在日志输出里。glog 初始化后才能用 `LOG(INFO)` 等宏。</span></span><br><span class="line">  google::InitGoogleLogging(<span class="string">&quot;Kuiper&quot;</span>);</span><br><span class="line">  </span><br><span class="line"><span class="comment">//设置日志文件输出目录。 glog 默认只输出到 stderr，设了这个就会同时写文件到 `./log/` 目录下</span></span><br><span class="line">  FLAGS_log_dir = <span class="string">&quot;./log/&quot;</span>;</span><br><span class="line">  </span><br><span class="line"><span class="comment">//日志同时输出到 stderr。 即使写了日志文件，也同时在终端打印</span></span><br><span class="line">  FLAGS_alsologtostderr = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//先打印一条日志</span></span><br><span class="line">  LOG(INFO) &lt;&lt; <span class="string">&quot;Start Test...\n&quot;</span>;</span><br><span class="line">  </span><br><span class="line"><span class="comment">//启动所有测试。 这会找到所有 `TEST()` 宏注册的测试用例，逐个运行，返回 0 表示全部通过，非 0 表示有失败。</span></span><br><span class="line">  <span class="keyword">return</span> RUN_ALL_TESTS();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行后日志为：</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">(base) liangji@ubun:~/AI_INFRA/projects/teacher/KuiperLLama/build/test$ ./test_llm --gtest_filter=test_buffer.*</span><br><span class="line"></span><br><span class="line">I20260703 13:25:19.062956 138410764066816 test_main.cpp:10]      Start Test...</span><br><span class="line">Note: Google Test filter = test_buffer.*</span><br><span class="line">[==========] Running 7 tests from 1 test suite.</span><br><span class="line">[----------] Global test environment set-up.</span><br><span class="line">[----------] 7 tests from test_buffer</span><br><span class="line">[ RUN      ] test_buffer.use_external1</span><br><span class="line">[       OK ] test_buffer.use_external1 (156 ms)</span><br></pre></td></tr></table></figure><h2 id="测试单元"><a href="#测试单元" class="headerlink" title="测试单元"></a>测试单元</h2><p>主程序的最后一句<br><code>return RUN_ALL_TESTS();</code></p><p>调用RUN_ALL_TESTS(), 这个就是GTest里面的函数，跳转到第三方库去执行所有注册了TEST()宏的测试用例</p><p>因为是宏函数，所以也不需要提前创建申请。GTest那边调用所有TEST宏函数</p><p>而我们注册的TEST(组名，用例名)，由于组名，用例名不同，刚好不冲突</p><blockquote><p>#include &lt;gtest&#x2F;gtest.h&gt;<br>所以，每个测试组，只要include这个头文件，就能注册TEST宏函数了？ 我记得宏函数不得#define TEST()这样来定义宏函数吗？他这个怎么这样就能实现注册？****</p></blockquote><p><img src="/images/Pasted%20image%2020260703133215.png"></p><p><strong>那么每个注册的TEST测试单元，要如何来判断该用例是否通过？</strong></p><p>不是靠 TEST 函数的返回值（它返回 <code>void</code>），而是靠<strong>断言宏在失败时往测试结果对象里写记录</strong>。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 全局的当前测试结果对象</span></span><br><span class="line"><span class="type">static</span> TestResult* current_test_result;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ASSERT_EQ 简化版</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ASSERT_EQ(a, b)                                                      \</span></span><br><span class="line"><span class="meta">  <span class="keyword">if</span> ((a) != (b)) &#123;                                                          \</span></span><br><span class="line"><span class="meta">    current_test_result-&gt;AddFailure(<span class="string">&quot;Expected &quot;</span> #a <span class="string">&quot; == &quot;</span> #b);               \</span></span><br><span class="line"><span class="meta">    return;  <span class="comment">// 立即终止当前测试                                             \</span></span></span><br><span class="line"><span class="comment"><span class="meta">  &#125;</span></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260703133530.png" alt="594"></p><h3 id="测试结果判断"><a href="#测试结果判断" class="headerlink" title="测试结果判断"></a>测试结果判断</h3><p>分两大类：</p><h4 id="ASSERT-失败立即终止当前测试"><a href="#ASSERT-失败立即终止当前测试" class="headerlink" title="ASSERT_失败立即终止当前测试"></a>ASSERT_失败立即终止当前测试</h4><table><thead><tr><th>断言</th><th>含义</th></tr></thead><tbody><tr><td><code>ASSERT_TRUE(cond)</code></td><td>cond 为 true</td></tr><tr><td><code>ASSERT_FALSE(cond)</code></td><td>cond 为 false</td></tr><tr><td><code>ASSERT_EQ(a, b)</code></td><td>a &#x3D;&#x3D; b</td></tr><tr><td><code>ASSERT_NE(a, b)</code></td><td>a !&#x3D; b</td></tr><tr><td><code>ASSERT_LT(a, b)</code></td><td>a &lt; b</td></tr><tr><td><code>ASSERT_LE(a, b)</code></td><td>a &lt;&#x3D; b</td></tr><tr><td><code>ASSERT_GT(a, b)</code></td><td>a &gt; b</td></tr><tr><td><code>ASSERT_GE(a, b)</code></td><td>a &gt;&#x3D; b</td></tr><tr><td><code>ASSERT_STREQ(s1, s2)</code></td><td>两个 C 字符串相等</td></tr><tr><td><code>ASSERT_FLOAT_EQ(a, b)</code></td><td>浮点数近似相等</td></tr><tr><td></td><td></td></tr></tbody></table><h4 id="EXPECT-失败继续执行"><a href="#EXPECT-失败继续执行" class="headerlink" title="EXPECT_ 失败继续执行"></a>EXPECT_ 失败继续执行</h4><p><code>EXPECT_TRUE</code>、<code>EXPECT_EQ</code>、<code>EXPECT_NE</code>……和上面一一对应，只是失败后不 <code>return</code>，继续往下跑。</p><p><strong>选择原则</strong></p><ul><li>一个测试里只需要检查一件事 → <code>ASSERT_*</code></li><li>一个测试里需要检查多个独立条件，想看所有失败项 → <code>EXPECT_*</code></li></ul><p>项目中只用 <code>ASSERT_*</code>，因为每个测试都很短，一个断言失败后面的预期也没意义了。</p>]]></content>
    
    
    <summary type="html">以 CUDA 推理项目为背景，记录 GTest 与 glog 的接入、测试组织、断言和日志使用。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="工程基础" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%B7%A5%E7%A8%8B%E5%9F%BA%E7%A1%80/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="GTest" scheme="https://liangji-seu.github.io/tags/GTest/"/>
    
    <category term="glog" scheme="https://liangji-seu.github.io/tags/glog/"/>
    
  </entry>
  
  <entry>
    <title>infra Transformer 推理性能分析</title>
    <link href="https://liangji-seu.github.io/2026/06/14/infra-Transformer%20%E6%8E%A8%E7%90%86%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90/"/>
    <id>https://liangji-seu.github.io/2026/06/14/infra-Transformer%20%E6%8E%A8%E7%90%86%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90/</id>
    <published>2026-06-14T15:51:25.000Z</published>
    <updated>2026-10-03T10:34:31.623Z</updated>
    
    <content type="html"><![CDATA[<p>这里主要总结一下，ai infra里面的各种术语，以及transformer的参数，计算，以及优化点的分析：</p><h1 id="ai-infra的各部分与transformer的对应关系"><a href="#ai-infra的各部分与transformer的对应关系" class="headerlink" title="ai infra的各部分与transformer的对应关系"></a>ai infra的各部分与transformer的对应关系</h1><p><img src="/images/Pasted%20image%2020260512192559.png"></p><p>主要分为：</p><ul><li>CUDA算子<ul><li>FlashAttention, 高效GEMM kernel</li><li>Fused Softmax,  online Softmax</li><li>LayerNorm kernel 融合</li></ul></li><li>分布式训练<ul><li>张量并行（层内多头并行）</li><li>流水线并行（decoder block堆叠）</li><li>ZeRO 显存优化（所有参数矩阵的存储与通信）</li></ul></li><li>推理部署<ul><li>KVcache管理</li><li>PagedAttention (KVcache的显存碎片)</li><li>量化（INT4&#x2F;INT8&#x2F;FP8）(所有权重矩阵和KVCache)</li></ul></li></ul><h1 id="主流大模型架构：Decoder-only"><a href="#主流大模型架构：Decoder-only" class="headerlink" title="主流大模型架构：Decoder-only"></a>主流大模型架构：Decoder-only</h1><p>主流大模型：GPT，LLaMA， Mistral, Qwen, Deepseek 都是编码器仅结构</p><p><img src="/images/Pasted%20image%2020260512193031.png" alt="697"></p><p>结构和传统的transformer的decoder块，稍有不同。<strong>这就是后面将会讲的 Pre-Norm vs Post-Norm 区别</strong></p><h2 id="encoder-only-encoder-decoder-decoder-only-对比"><a href="#encoder-only-encoder-decoder-decoder-only-对比" class="headerlink" title="encoder-only &#x2F; encoder-decoder &#x2F; decoder-only 对比"></a>encoder-only &#x2F; encoder-decoder &#x2F; decoder-only 对比</h2><p><img src="/images/Pasted%20image%2020260512193141.png"></p><p>我们的大模型的主流都是Decoder-only, 因为目标都是自回归</p><h1 id="decoder-only-block解析"><a href="#decoder-only-block解析" class="headerlink" title="decoder-only block解析"></a>decoder-only block解析</h1><h2 id="self-attention"><a href="#self-attention" class="headerlink" title="self-attention"></a>self-attention</h2><p>这里先要强调，主流大模型，输入block的X的embed_size 就等于 hidden_size &#x3D; d_model</p><p>所以X（batch_size, seq_len, d_model）, </p><h3 id="QKV线性投影"><a href="#QKV线性投影" class="headerlink" title="QKV线性投影"></a>QKV线性投影</h3><p>x 经过：</p><ul><li>W_Q   (d_model, d_model)</li><li>W_K    (d_model, d_model)</li><li>W_V    (d_model, d_model)<br>得到我们的Q，K, V</li></ul><p>这一步就是多头的里面，统一升维，不过还是从d_model -&gt; d_model</p><p>所以这一步，会有<strong>3个指令的运算</strong><br><img src="/images/Pasted%20image%2020260512193547.png"></p><h4 id="优化点：CUDA优化，张量并行（多头）"><a href="#优化点：CUDA优化，张量并行（多头）" class="headerlink" title="优化点：CUDA优化，张量并行（多头）"></a>优化点：CUDA优化，张量并行（多头）</h4><h3 id="注意力计算"><a href="#注意力计算" class="headerlink" title="注意力计算"></a>注意力计算</h3><h4 id="注意力分数"><a href="#注意力分数" class="headerlink" title="注意力分数"></a>注意力分数</h4><p><img src="/images/Pasted%20image%2020260512193737.png"></p><h4 id="缩放"><a href="#缩放" class="headerlink" title="缩放"></a>缩放</h4><p><img src="/images/Pasted%20image%2020260512193809.png"></p><p>把方差拉回1</p><h4 id="softmax"><a href="#softmax" class="headerlink" title="softmax"></a>softmax</h4><p><img src="/images/Pasted%20image%2020260512193831.png"></p><h5 id="优化点：softmax算子"><a href="#优化点：softmax算子" class="headerlink" title="优化点：softmax算子"></a>优化点：softmax算子</h5><p>softmax标准实现，需要对一个q的所有k的打分做两边扫描</p><p>online softmax: 合并成一遍扫描<br>FlashAttention: 基于online softmax 快速计算attention</p><h4 id="加权求和得到V"><a href="#加权求和得到V" class="headerlink" title="加权求和得到V"></a>加权求和得到V</h4><p><img src="/images/Pasted%20image%2020260512194046.png"></p><h4 id="输出投影"><a href="#输出投影" class="headerlink" title="输出投影"></a>输出投影</h4><p>合并多头，输出d_model维度<br><img src="/images/Pasted%20image%2020260512194116.png"></p><h3 id="FFN"><a href="#FFN" class="headerlink" title="FFN"></a>FFN</h3><p>前面attention负责让信息交互，FFN负责对每个信息token做深度加工</p><p><img src="/images/Pasted%20image%2020260512195836.png"></p><h4 id="模型参数量分析"><a href="#模型参数量分析" class="headerlink" title="模型参数量分析"></a>模型参数量分析</h4><p><img src="/images/Pasted%20image%2020260512195905.png"></p><p>attention:  embed_size &#x3D; d_model </p><p>FFN（标准）， 两层，且d_ff&#x3D; 4d_model</p><p>AI Infra 关联：由于 FFN 的参数量占大头，在做张量并行时，FFN 的切分方式直接影响通信开销。Megatron-LM 将 W1W1​ 按列切分、W2W2​ 按行切分，使得中间结果不需要 AllReduce，只在最后做一次 AllReduce——这种切分方式正是利用了 FFN 的”先升维后降维”结构。在混合专家模型（MoE）中，FFN 进一步被拆分为多个”专家”，引入了 Expert Parallelism 这一新的并行维度。</p><h3 id="激活函数的变化"><a href="#激活函数的变化" class="headerlink" title="激活函数的变化"></a>激活函数的变化</h3><ul><li>ReLU</li><li>GELU</li><li>SwiGLU<br><img src="/images/Pasted%20image%2020260512200051.png"></li></ul><h2 id="位置编码"><a href="#位置编码" class="headerlink" title="位置编码"></a>位置编码</h2><h3 id="Sinusoidal位置编码（原始）"><a href="#Sinusoidal位置编码（原始）" class="headerlink" title="Sinusoidal位置编码（原始）"></a>Sinusoidal位置编码（原始）</h3><p><img src="/images/Pasted%20image%2020260512200138.png"></p><p><img src="/images/Pasted%20image%2020260512200158.png"></p><h3 id="RoPE（旋转位置编码）（主流选择）"><a href="#RoPE（旋转位置编码）（主流选择）" class="headerlink" title="RoPE（旋转位置编码）（主流选择）"></a>RoPE（旋转位置编码）（主流选择）</h3><p><strong>核心思想</strong>：不在 embedding 层注入位置信息，而是在计算 Attention 时，通过<strong>旋转</strong> Q 和 K 向量来编码位置。</p><p>具体做法是把 Q 和 K 向量的每两个相邻维度看作二维平面上的坐标，然后根据 token 的位置按特定角度旋转这个二维向量：<br><img src="/images/Pasted%20image%2020260512200339.png" alt="599"></p><p>这样做的关键性质是：<strong>旋转后的 Q 和 K 做内积时，结果只依赖于两个 token 的相对位置差</strong></p><p>这意味着 <strong>Attention 天然编码了相对位置信息</strong>，非常符合语言理解的需求（“第 3 个词和第 5 个词之间的关系”比”第 3 个词和第 5 个词各自的绝对位置”更重要）</p><p><img src="/images/Pasted%20image%2020260512200445.png"></p><p><img src="/images/Pasted%20image%2020260512200529.png"></p><h3 id="LayerNorm-残差连接"><a href="#LayerNorm-残差连接" class="headerlink" title="LayerNorm, 残差连接"></a>LayerNorm, 残差连接</h3><p>LayerNorm（Layer Normalization）对每个 token 的特征向量做归一化——减去均值、除以标准差，再通过可学习的缩放参数 γγ 和偏移参数 ββ 恢复表达能力<br><img src="/images/Pasted%20image%2020260512200746.png"></p><h4 id="Pre-Norm-Post-Norm"><a href="#Pre-Norm-Post-Norm" class="headerlink" title="Pre-Norm &#x2F;  Post- Norm"></a>Pre-Norm &#x2F;  Post- Norm</h4><p>LayerNorm 放在子层的前面还是后面，是一个看似微小但影响深远的设计选择</p><p>经典的transformer是放在后面，post norm<br><img src="/images/Pasted%20image%2020260512200909.png"></p><p>当前大模型主流是pre-norm</p><p><img src="/images/Pasted%20image%2020260512200903.png"></p><p>关键区别是残差的高速通路不经过LayerNorm处理</p><p><img src="/images/Pasted%20image%2020260512200959.png"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><img src="/images/Pasted%20image%2020260512201428.png"></p><p><img src="/images/Pasted%20image%2020260512201735.png"></p><p>注意这里，位置编码是在输入后，先进入层归一化，然后进入掩码自注意力的时候，先升维，然后再模型的语义空间里面，进行<strong>位置编码</strong>的</p><h2 id="LLaMA-2-7B-为例，跟踪梯度"><a href="#LLaMA-2-7B-为例，跟踪梯度" class="headerlink" title="LLaMA-2-7B 为例，跟踪梯度"></a>LLaMA-2-7B 为例，跟踪梯度</h2><p><img src="/images/Pasted%20image%2020260512201551.png"><img src="/images/Pasted%20image%2020260512201601.png"></p><p><img src="/images/Pasted%20image%2020260512202443.png"><br><img src="/images/Pasted%20image%2020260512202453.png"></p><h3 id="KVcache"><a href="#KVcache" class="headerlink" title="KVcache"></a>KVcache</h3><p><img src="/images/Pasted%20image%2020260512203914.png"></p><h3 id="Prefill-和-Decode-两阶段"><a href="#Prefill-和-Decode-两阶段" class="headerlink" title="Prefill 和 Decode 两阶段"></a>Prefill 和 Decode 两阶段</h3><p>prefill： 预填充阶段：(<strong>初始KVcache</strong>）<strong>(计算瓶颈</strong>)<br>处理用户输入的整个 prompt。所有输入 token 可以并行计算，一次前向传播就生成所有 token 的 K、V，并缓存起来。</p><pre><code>Prefill 阶段的矩阵运算 batch 维度大（NN 个 token 一起算），是典型的 **Compute Bound**（算力瓶颈）操作。它的耗时决定了 TTFT（Time To First Token，首 token 延迟）。</code></pre><p>decode(解码)：逐个生成输出 token。每步只有 1 个新 token 的 Q 去和所有历史 K 做 Attention，新 token 的 K、V 追加到缓存中（<strong>带宽瓶颈</strong>）</p><p>Decode 阶段每步只有 1 个 token 的计算量，矩阵乘法退化为矩阵-向量乘，GPU 的算力远远用不满，大部分时间花在从 HBM 搬运 KV Cache 数据上。这是典型的 <strong>Memory Bound</strong>（带宽瓶颈）操作。它的耗时决定了 TPOT（Time Per Output Token，<code>每 token 延迟</code>）。</p><p><img src="/images/Pasted%20image%2020260512204317.png"><br>这就是PD分离</p><h4 id="KVCache开销分析"><a href="#KVCache开销分析" class="headerlink" title="KVCache开销分析"></a>KVCache开销分析</h4><p>KV Cache 的做法是：把每一层、每一步算出的 K 和 V 缓存在 GPU 显存中。Decode 时，新 token 只需要计算自己的 Q、K、V，然后把新的 K、V 追加到缓存中，Attention 计算使用完整的缓存 K、V。</p><p><img src="/images/Pasted%20image%2020260512204450.png"></p><p><img src="/images/Pasted%20image%2020260512205203.png"><br>所以这里，为了减少推理的显存占用：<br><img src="/images/4e33e23c76217fb7f29a560c64487309.jpg"></p><h2 id="decoder-only-的不同"><a href="#decoder-only-的不同" class="headerlink" title="decoder-only 的不同"></a>decoder-only 的不同</h2><p><img src="/images/Pasted%20image%2020260512234211.png"></p><p>可以发现，Decoder-only最后是束搜索</p><h2 id="llama2"><a href="#llama2" class="headerlink" title="llama2"></a>llama2</h2><p><img src="/images/Pasted%20image%2020260512234442.png"></p><h1 id="术语"><a href="#术语" class="headerlink" title="术语"></a>术语</h1><p>GEMM：矩阵乘法</p><p>矩阵乘法的计算复杂度：A(n，m） x B (m, d)  计算量O(nxmxd)<br><img src="/images/Pasted%20image%2020260512194319.png"></p><p>FlashAttention:<br>计算attention的缩放点积的点积的时候<br>标准实现：完整的NxN的注意力权重，写到HBM显存种<br> FlashAttention：通过tiling分块计算+online softmax, 让注意力矩阵全部在SRAM中，把访存从O(N2)降到O(N), 计算量没变，占用内存大幅减少<br> “Memory-aware”优化的核心思想<br><img src="/images/Pasted%20image%2020260512195032.png" alt="617"></p><p>是通过分块，来触发缓存机制</p><p>参数量计算：<br><img src="/images/Pasted%20image%2020260512195554.png"><br>16M 是 4096x4096 &#x2F; 10^6得来的</p><p><strong>Megatron-LM 张量并行</strong>：<br> 多头注意力<br> 32 个头可以均匀分配到多张 GPU 上——比如 4 张卡各处理 8 个头，每张卡只需要 1&#x2F;4 的 QKV 权重和计算量。这就是 Megatron-LM 张量并行的核心思想：沿着”头”的维度切分 Attention 模块。切分之后只需要一次 AllReduce 通信就能将各卡的部分结果汇总<br><br><br>Attention 头数的变种——<strong>MQA（Multi-Query Attention）</strong> 让所有头共享一组 KV、<strong>GQA（Grouped-Query Attention）</strong> 让若干头共享一组 KV——直接影响 KV Cache 的大小和张量并行的切分方式，是推理优化中的核心概念。</p><p>算子<br><img src="/images/Pasted%20image%2020260512201333.png"></p><p>kernel<br><img src="/images/Pasted%20image%2020260512201325.png"></p><p><strong>Weight Tying（权重共享）</strong><br>说的是，词嵌入和词返回的权重是共享的</p>]]></content>
    
    
    <summary type="html">梳理 Decoder-only Transformer 的结构、KV Cache、注意力与 FFN，并连接 CUDA 算子和模型并行等推理优化。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="基础与训练" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%9F%BA%E7%A1%80%E4%B8%8E%E8%AE%AD%E7%BB%83/"/>
    
    
    <category term="Transformer" scheme="https://liangji-seu.github.io/tags/Transformer/"/>
    
    <category term="KV Cache" scheme="https://liangji-seu.github.io/tags/KV-Cache/"/>
    
    <category term="FlashAttention" scheme="https://liangji-seu.github.io/tags/FlashAttention/"/>
    
    <category term="并行计算" scheme="https://liangji-seu.github.io/tags/%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/"/>
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
  </entry>
  
  <entry>
    <title>infra CUDA编程</title>
    <link href="https://liangji-seu.github.io/2026/06/14/infra-CUDA%E7%BC%96%E7%A8%8B/"/>
    <id>https://liangji-seu.github.io/2026/06/14/infra-CUDA%E7%BC%96%E7%A8%8B/</id>
    <published>2026-06-14T15:51:25.000Z</published>
    <updated>2026-10-03T10:34:31.621Z</updated>
    
    <content type="html"><![CDATA[<h1 id="GPU内存模型"><a href="#GPU内存模型" class="headerlink" title="GPU内存模型"></a>GPU内存模型</h1><p>首先，GPU是单任务，多数据的类型（同一套代码指令，输入不同的数据）</p><p>所以，你编写一个核函数，这个核函数会被执行很多次，输入不同的数据，这个一个核函数的执行，就是一个thread。</p><p>编程架构：</p><ul><li>grid</li><li>block</li><li>warp</li><li>thread</li></ul><p>block之间相互独立，thread是最小执行单元，warp是一些thread的打包，是最小调度单元</p><p>一个thread，他的内存模型是：<br><img src="/images/Pasted%20image%2020260515221949.png"><br>（这里和CPU的内存模型不一样的是，CPU的进程之间是不考虑共享的，只考虑调度）<br>（GPU上的thread，是考虑共享的，而且每个部分存放的位置也不如CPU上的进程存放的整齐）</p><p>一个thread, 由：</p><ul><li><p><strong>寄存器</strong></p><ul><li>其实是当栈区来用的</li></ul></li><li><p><strong>局部内存 (Local Memory)</strong></p><ul><li><strong>存储内容：</strong> 1. <strong>寄存器溢出 (Spilling)：</strong> 当你写的代码太复杂，寄存器不够用了，编译器会把多出来的变量偷偷挪到这里。 2. <strong>大数组：</strong> 在线程内部定义的、无法完全放入寄存器的局部数组。</li><li><strong>注意：</strong> 它是<strong>逻辑局部，物理全局</strong>。虽然名字带 Local，但它跑在显存（HBM&#x2F;VRAM）上，访问它和访问 Global Memory 一样慢。</li><li>其实是<strong>充当栈区的补充</strong></li></ul></li><li><p><strong>共享内存</strong></p><ul><li>是用来通信的</li></ul></li><li><p><strong>全局内存</strong></p><ul><li>全局区，<strong>数据段</strong>，<strong>rwdata</strong></li><li>是用来输入数据，输出结果的</li></ul></li><li><p><strong>常量内存</strong></p><ul><li>显存中，<strong>只读数据段</strong>, <strong>rodata</strong></li></ul></li><li><p><strong>纹理&#x2F;表面内存</strong></p><ul><li>显存中，针对2d&#x2F;3d的特殊用途</li></ul></li><li><p><strong>L1缓存</strong></p><ul><li>片上，（SM内），缓存L2缓存</li></ul></li><li><p><strong>L2 缓存</strong></p><ul><li>片外（SM外，GPU内），所有SM共享，缓存显存</li></ul></li></ul><table><thead><tr><th><strong>GPU 存储介质</strong></th><th><strong>你的理解</strong></th><th><strong>对应传统内存段</strong></th><th><strong>核心技术细节补遗</strong></th></tr></thead><tbody><tr><td><strong>寄存器 (Registers)</strong></td><td><strong>栈区 (主)</strong></td><td><strong>Stack &#x2F; CPU Registers</strong></td><td>线程执行的第一优先级。不可取地址（No pointers）。</td></tr><tr><td><strong>局部内存 (Local)</strong></td><td><strong>栈区补充</strong></td><td><strong>Stack (Overflow)</strong></td><td>当局部数组下标是动态变量，或寄存器不够（Spilling）时使用。</td></tr><tr><td><strong>共享内存 (Shared)</strong></td><td><strong>通信用</strong></td><td><strong>IPC &#x2F; User Cache</strong></td><td>逻辑上类似一个“手动控制的 L1”，用于 Block 内线程协作。</td></tr><tr><td><strong>全局内存 (Global)</strong></td><td><strong>全局&#x2F;数据段</strong></td><td><strong>.data &#x2F; .bss &#x2F; Heap</strong></td><td>CPU 与 GPU 交换数据的唯一窗口，容量最大，延迟最高。</td></tr><tr><td><strong>常量内存 (Constant)</strong></td><td><strong>只读数据段</strong></td><td><strong>.rodata</strong></td><td>物理上在显存，但 SM 内部有专用 Cache，全 Warp 读取同一地址时极快。</td></tr><tr><td><strong>纹理内存 (Texture)</strong></td><td><strong>特殊用途</strong></td><td><strong>Video Memory</strong></td><td>硬件自带插值、边界处理能力，具有 2D&#x2F;3D 空间局部性缓存。</td></tr><tr><td><strong>L1 缓存 (L1)</strong></td><td><strong>片上缓存</strong></td><td><strong>L1 Cache</strong></td><td>现代架构中，L1 常与 Shared Memory 共享同一块硬件 SRAM 资源。</td></tr><tr><td><strong>L2 缓存 (L2)</strong></td><td><strong>片外缓存</strong></td><td><strong>L2 Cache</strong></td><td>所有 SM 共享，是显存之前的最后一道高速防线。</td></tr></tbody></table><p>下图展示了并行计算的内存模型，在物理硬件上的分布</p><p><img src="/images/Untitled%20Diagram.drawio%20(2).png"></p><h1 id="CUDA编程"><a href="#CUDA编程" class="headerlink" title="CUDA编程"></a>CUDA编程</h1><h2 id="核函数（kernel）"><a href="#核函数（kernel）" class="headerlink" title="核函数（kernel）"></a>核函数（kernel）</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">__global__ <span class="type">void</span> <span class="title function_">fun</span><span class="params">()</span>&#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核函数，就是thread里面执行的内容，根据输入的数据不同，产生同样的计算行为，输出不同的结果。</p><p>每个核函数内部可以访问：</p><ul><li>blockIdx.x (block的id号：0 ~ gridDim-1)</li><li>blockDim.x ( 就是获取blockDim)</li><li>threadIdx.x （thread的id号，0 ~ blockDim-1）</li></ul><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 声明核函数</span></span><br><span class="line">__global__ <span class="type">void</span> <span class="title function_">myKernel</span><span class="params">(<span class="type">float</span>* data, <span class="type">int</span> n)</span> &#123;</span><br><span class="line">    <span class="type">int</span> idx = blockIdx.x * blockDim.x + threadIdx.x;</span><br><span class="line">    <span class="keyword">if</span> (idx &lt; n) &#123;</span><br><span class="line">        data[idx] *= <span class="number">2.0f</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用核函数：128 个 Block，每个 Block 256 个线程</span></span><br><span class="line">myKernel&lt;&lt;&lt;<span class="number">128</span>, <span class="number">256</span>&gt;&gt;&gt;(data, n);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们launch一次kernel，产生一个grid，表示并行任务的集合<br>block是128个，每个block内部由256个thread</p><p>注意看，这里的float* data, 这个指针变量在寄存器区，但是指向的内容在显存（全局内存区），所以每次thread读取和输出，都要访存，效率很差</p><h2 id="修饰符："><a href="#修饰符：" class="headerlink" title="修饰符："></a>修饰符：</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">__global__ 指明函数是核函数，CPU调用，GPU上执行</span><br><span class="line">__device__ 指明设备函数，GPU上被核函数/其他设备函数调用执行</span><br><span class="line">__host__ 普通函数，CPU上执行</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">__shared__ 指明内存在共享内存上，否则会在栈区（SM的寄存器区）</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="维度索引"><a href="#维度索引" class="headerlink" title="维度索引"></a>维度索引</h2><p>对于硬件来说，她并不关心thead, block的排布，在内存上都是顺序存放的。<br>所以这里的维度，索引，仅仅只是索引方式的变化</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">//一维索引&lt;&lt;&lt;gridDim, blockDim&gt;&gt;&gt;</span></span><br><span class="line">&lt;&lt;&lt;<span class="number">128</span>, <span class="number">256</span>&gt;&gt;&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">//二维索引/三维索引，使用dim3来构造我们的blockDim, gridDim</span></span><br><span class="line">dim3 <span class="title function_">blockDim</span><span class="params">(<span class="number">16</span>, <span class="number">16</span>)</span> <span class="comment">//一个block里面16x16个thread</span></span><br><span class="line">dim3 <span class="title function_">gridDim</span><span class="params">((width+<span class="number">15</span>)/<span class="number">16</span>,(height+<span class="number">15</span>)/<span class="number">16</span>)</span> <span class="comment">//这里的width, height 是thread阵列的形状</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260516102414.png"></p><h2 id="内存管理"><a href="#内存管理" class="headerlink" title="内存管理"></a>内存管理</h2><p>cuda需要显示管理CPU的内存，还有显存</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="type">float</span> *d_data; <span class="comment">//d_ 表示设备端，也就是显存</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//这个</span></span><br><span class="line">cudaMalloc(&amp;d_data, n*<span class="keyword">sizeof</span>(<span class="type">float</span>)); <span class="comment">//在显存上分配n个float空间</span></span><br><span class="line"><span class="comment">// 这个分配动作是CPU发起的，CUDA驱动程序在GPU的全局内存上分配的</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">cudaMemcpy(d_data, h_data, n*<span class="keyword">sizeof</span>(<span class="type">float</span>), cudaMemcpyHostToDevice);</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">第四个参数cudaMemcpyKind</span></span><br><span class="line"><span class="comment">- cudaMemcpyHostToDevice</span></span><br><span class="line"><span class="comment">- cudaMemcpyDeviceToHost</span></span><br><span class="line"><span class="comment">- cudaMemcpyDeviceToDevice</span></span><br><span class="line"><span class="comment">- cudaMemcpyDefault</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line">myKernel&lt;&lt;&lt;gridDim, blockDim&gt;&gt;&gt;(d_data, n);</span><br><span class="line"></span><br><span class="line">cudaMemcpy(h_data, d_data, n*<span class="keyword">sizeof</span>(<span class="type">float</span>), cudaMemcpyDeviceToHost);</span><br><span class="line"></span><br><span class="line">cudaFree(d_data);</span><br></pre></td></tr></table></figure><h2 id="异步传输与stream"><a href="#异步传输与stream" class="headerlink" title="异步传输与stream"></a>异步传输与stream</h2><p>正常情况下，cudaMemcpy 是用cpu进行拷贝的，所以会阻塞，我们希望更高效的使用异步传输，也就是非阻塞，比如DMA</p><p><code>cudaMemcpy</code> 的本质就是通过 PCIe 总线进行的 DMA（Direct Memory Access）传输</p><p>用Stream可以实现异步传输与计算难道重叠</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">cudaStream_t stream;</span><br><span class="line">cudaStreamCreate(&amp;stream);</span><br><span class="line"></span><br><span class="line">cudaMemcpyAsync(d_data, h_data, size, cudaMemcpyHostToDevice, stream);</span><br><span class="line"></span><br><span class="line">myKernel&lt;&lt;&lt;gridDim, blockDim, <span class="number">0</span>, stream&gt;&gt;&gt;(d_data, n);</span><br><span class="line"></span><br><span class="line">cudaMemcpyAsync(h_data, d_data, size, cudaMemcpyDeviceToHost, stream);</span><br><span class="line"></span><br><span class="line">cudaStreamSynchronize(stream);</span><br><span class="line">cudaStreamDestroy(stream);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在操作系统里面，我们申请的内存，叫做<strong>可分页内存</strong>，逻辑上是连续的虚拟地址，物理上是分在不连续的物理也框里面的。可能会被swap到磁盘上。</p><p>这对于软件没事，顶多等一等磁盘，重新SWAP回来。</p><p>但是DMA只知道物理地址，所以物理地址不能改变，且这块内存不能被swap到磁盘。</p><p><img src="/images/Pasted%20image%2020260516103838.png"><br><img src="/images/Pasted%20image%2020260516103914.png"></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="type">float</span>* h_pinned;</span><br><span class="line">cudaMallocHost(&amp;h_pinned, size); <span class="comment">//分配锁页内存</span></span><br><span class="line">cudaFreeHost(h_pinned);</span><br></pre></td></tr></table></figure><h2 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> CUDA_CHECK(call) do &#123; \</span></span><br><span class="line"><span class="meta">    cudaError_t err = call; \</span></span><br><span class="line"><span class="meta">    <span class="keyword">if</span> (err != cudaSuccess) &#123; \</span></span><br><span class="line"><span class="meta">        fprintf(stderr, <span class="string">&quot;CUDA error at %s:%d: %s\n&quot;</span>, \</span></span><br><span class="line"><span class="meta">                __FILE__, __LINE__, cudaGetErrorString(err)); \</span></span><br><span class="line"><span class="meta">        exit(EXIT_FAILURE); \</span></span><br><span class="line"><span class="meta">    &#125; \</span></span><br><span class="line"><span class="meta">&#125; while(0)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line">CUDA_CHECK(cudaMalloc(&amp;d_data, size));</span><br><span class="line">CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice));</span><br><span class="line"></span><br><span class="line"><span class="comment">// kernel 启动后检查错误</span></span><br><span class="line">myKernel&lt;&lt;&lt;gridDim, blockDim&gt;&gt;&gt;(d_data, n);</span><br><span class="line">CUDA_CHECK(cudaGetLastError());           <span class="comment">// 检查启动参数错误</span></span><br><span class="line">CUDA_CHECK(cudaDeviceSynchronize());      <span class="comment">// 检查执行错误</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="thread-障碍同步"><a href="#thread-障碍同步" class="headerlink" title="thread 障碍同步"></a>thread 障碍同步</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">__global__ <span class="type">void</span> <span class="title function_">sharedMemDemo</span><span class="params">(<span class="type">float</span>* input, <span class="type">float</span>* output, <span class="type">int</span> n)</span> &#123;</span><br><span class="line">    <span class="comment">// 静态分配共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> smem[<span class="number">256</span>];</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> idx = blockIdx.x * blockDim.x + threadIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 从全局内存加载到共享内存</span></span><br><span class="line">    <span class="keyword">if</span> (idx &lt; n) &#123;</span><br><span class="line">        smem[threadIdx.x] = input[idx];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 同步：确保所有线程都加载完毕</span></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 从共享内存读取（可以读邻居的数据，速度极快）</span></span><br><span class="line">    <span class="keyword">if</span> (idx &lt; n &amp;&amp; threadIdx.x &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        output[idx] = smem[threadIdx.x] + smem[threadIdx.x - <span class="number">1</span>];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里的__syncthreads()</p><p>thread运行到这里就阻塞，等待block上的所有的thread都运行到这里，才一起往下</p><h2 id="静态共享内存-动态共享内存"><a href="#静态共享内存-动态共享内存" class="headerlink" title="静态共享内存&#x2F;动态共享内存"></a>静态共享内存&#x2F;动态共享内存</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">__global__ <span class="type">void</span> <span class="title function_">sharedMemDemo</span><span class="params">(<span class="type">float</span>* input, <span class="type">float</span>* output, <span class="type">int</span> n)</span> &#123;</span><br><span class="line">    <span class="comment">// 静态分配共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> smem[<span class="number">256</span>];</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">extern</span> __shared__ <span class="type">float</span> dynamic_smem[];</span><br><span class="line"></span><br><span class="line">kernel&lt;&lt;&lt;gridDim, blockDim, sharedMemBytes&gt;&gt;&gt;(args);</span><br><span class="line"><span class="comment">//这个分配动作在 **Kernel 启动的那一刻**就已经由硬件和驱动程序帮你完成了。</span></span><br></pre></td></tr></table></figure><h2 id="全局内存"><a href="#全局内存" class="headerlink" title="全局内存"></a>全局内存</h2><p>全局内存就是”显存”，容量最大但速度最慢。<code>cudaMalloc</code> 分配的内存、kernel 参数中的指针都指向全局内存。</p><p><strong>合并访问（Coalesced Access）</strong> 是全局内存优化的黄金法则：同一个 Warp 内的 32 个线程应该访问连续的内存地址，这样硬件可以将多次访问合并为少量内存事务</p><blockquote><p>因为warp是一起执行的，他们都需要输入对吧，都需要输出对吧，那么肯定要一整块的拷贝内存到L1缓存才快，所以，要合并访问</p></blockquote><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 合并访问 </span></span><br><span class="line"><span class="type">float</span> val = data[threadIdx.x];  <span class="comment">//缓存会一口气拷贝data的一整块内容</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 坏, 因为不是顺序访问，很可能这次缓存的内容，下一个不在里面</span></span><br><span class="line"><span class="type">float</span> val = data[threadIdx.x * stride]; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 随机访问，最坏，完全无法合并</span></span><br><span class="line"><span class="type">float</span> val = data[random_index[threadIdx.x]];</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>数据布局优化： SoA</strong></p><p>通俗说，就是按照属性合并</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// AoS (Array of Structures) —— 对 GPU 不友好</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Particle</span> &#123;</span> <span class="type">float</span> x, y, z, w; &#125;;</span><br><span class="line">Particle particles[N];</span><br><span class="line"><span class="comment">// 访问所有 x：particles[0].x, particles[1].x, ... 跨步为 16B</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// SoA (Structure of Arrays) —— 对 GPU 友好</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Particles</span> &#123;</span></span><br><span class="line">    <span class="type">float</span> x[N];  <span class="comment">// 所有 x 连续存放</span></span><br><span class="line">    <span class="type">float</span> y[N];</span><br><span class="line">    <span class="type">float</span> z[N];</span><br><span class="line">    <span class="type">float</span> w[N];</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 访问所有 x：连续内存，完美合并</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="常量内存"><a href="#常量内存" class="headerlink" title="常量内存"></a>常量内存</h2><p>这里的常量内存，的常量，其实是对GPU来说的，CPU来说还是可以写入的</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">__constant__ <span class="type">float</span> coefficients[<span class="number">256</span>];</span><br><span class="line"></span><br><span class="line">cudaMemcpyToSymbol(coefficients, h_coeffs, <span class="number">256</span>* <span class="keyword">sizeof</span>(<span class="type">float</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// GPU 端读取（所有线程读同一地址时最高效）</span></span><br><span class="line"><span class="type">float</span> c = coefficients[idx];</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260516105808.png"><img src="/images/Pasted%20image%2020260516110027.png"></p><h2 id="warp"><a href="#warp" class="headerlink" title="warp"></a>warp</h2><p>warp是GPU的最小调度单位，&#x3D;32个thread，他们一起执行相同的指令来处理数据。</p><p>如果出现分支，那么两个分支就必须串行执行，性能减半。</p><h3 id="warp级原语"><a href="#warp级原语" class="headerlink" title="warp级原语"></a>warp级原语</h3><p>warp内的线程，可以直接交换<strong>寄存器的数据</strong>，连共享内存都不用</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">0xFFFFFFFF 32位成员掩码， 指定warp中哪些thread参加此次同步</span></span><br><span class="line"><span class="comment">myVal 源数据， 每个thread的局部变量myVal, 在各自的register里面</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//同warp内，向下洗牌，第i个，拿i+delta个thread的myval</span></span><br><span class="line"><span class="type">float</span> val = __shfl_down_sync(<span class="number">0xFFFFFFFF</span>, myVal, delta);</span><br><span class="line"></span><br><span class="line"><span class="comment">//thread_i, 获得 thread_i ^ mask的 myVal值</span></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">目标threadIdx = 当前threadIdx 异或 mask</span></span><br><span class="line"><span class="comment">0xFFFFFFFF 是参加者名单，意思是一个 Warp 里的 **32 个线程全都要参加**这次大合唱， 如果少了一个，整个warp会被等待阻塞在哪里。</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">如果当前的thread执行了float val = __shfl_xor_sync(0xFFFFFFFF, myVal, mask);</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">对于这个thread来说，他看到0xFFFFFFFF , 直到这次获取数据，所有的thread都可以使用（虽然实际上其他thread可能没有），然后他吧自己的ID，和mask异或， 得到目标的thread，他看到这里的0xFFFFFFFF, 直到他是可以用的，就会阻塞等待，直到这个目标thread也执行到这一步，然后对面也把自己的ID和mask异或得到我这个thread，此时才能发生两个thread交换数据</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="type">float</span> val = __shfl_xor_sync(<span class="number">0xFFFFFFFF</span>, myVal, mask);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">这就是典型的 Warp 级 All-Reduce</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="type">float</span> sum = __reduce_add_sync(<span class="number">0xFFFFFFF</span>, myVal);</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260516112359.png"><img src="/images/Pasted%20image%2020260516112441.png"><img src="/images/Pasted%20image%2020260516112519.png"><img src="/images/Pasted%20image%2020260516112746.png"><img src="/images/Pasted%20image%2020260516112813.png"><br><img src="/images/Pasted%20image%2020260516112842.png"><br>具体mask是有交换规则的。</p><h2 id="共享内存的bank"><a href="#共享内存的bank" class="headerlink" title="共享内存的bank"></a>共享内存的bank</h2><p>这里先解释一下共享内存的bank，也就是数据在shared memory上是如何存储的</p><p><img src="/images/Pasted%20image%2020260516134140.png"><br>可以看到，</p><ul><li><strong>第0个4字节，在bank0</strong></li><li>第一个4字节，在bank1</li><li>第二个4字节，在bank2</li><li>…</li><li>第31个4字节，在bank31</li><li><strong>第32个4字节，在bank0</strong></li></ul><p>所以这就会带来bank conflict问题</p><p>首先，同一个block的所有thread共享一块共享内存空间。</p><p>但是bank conflict 仅仅会发生在warp内，不同warp的thread不会争抢一个bank，下面解释原因</p><p>在物理层面，<strong>一个 SM 内部的共享内存（Shared Memory）逻辑单元，在一个时钟周期内通常只能响应一个 Warp 的访存请求</strong></p><ul><li><p><strong>Block 之间是并发的</strong>：它们共用 SM 资源，不分先后。</p></li><li><p><strong>Warp 之间是独立调度的</strong>：调度器只选当下最能跑的 Warp。</p></li><li><p><strong>资源是硬伤</strong>：如果你一个 Block 占用了 100% 的共享内存，那这个 SM 就只能跑这一个 Block。这时候才会有你说的“顺序关系”——等这个 Block 跑完了撤场，下一个 Block 才能进来。</p></li></ul><p>所以回到同一个warp内的bank conflict</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//每一行 smem[i][]，的每一个元素，分别存放在不同的bank</span></span><br><span class="line"><span class="comment">//所以，smem[0][j], smem[1][j],...存放在第j个bank里面</span></span><br><span class="line">__shared__ <span class="type">float</span> smem[<span class="number">32</span>][<span class="number">32</span>]; </span><br><span class="line"><span class="type">float</span> val = smem[<span class="number">0</span>][threadIdx.x]; <span class="comment">//访问不同的bank， ok</span></span><br><span class="line"></span><br><span class="line"><span class="type">float</span> val = smem[threadIdx.x][<span class="number">0</span>]; <span class="comment">//所有thread 同时访问bank0</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//经典解决方案： padding</span></span><br><span class="line">__shared__ <span class="type">float</span> smem[<span class="number">32</span>][<span class="number">33</span>];  <span class="comment">//多加了一列，刚好保证32个thread错开了bank</span></span><br><span class="line"><span class="type">float</span> val = smem[threadIdx.x][<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="occupancy"><a href="#occupancy" class="headerlink" title="occupancy"></a>occupancy</h2><p>即实际活跃warp数&#x2F;SM最大warp数</p><p><img src="/images/Pasted%20image%2020260516141034.png"></p><p><img src="/images/Pasted%20image%2020260516141056.png"></p><h2 id="总结GPU的流，异步，同步的区别"><a href="#总结GPU的流，异步，同步的区别" class="headerlink" title="总结GPU的流，异步，同步的区别"></a>总结GPU的流，异步，同步的区别</h2><p>GPU通过多个流来实现并行，单个流里面，相当于顺序的工作队列，只能顺序执行，同步异步这里的概念都是针对cpu，同步的数据拷贝，该进程会阻塞，具体是进入睡眠等待cuda驱动拷贝完唤醒吗？异步的启动核函数，就是相当于在cuda驱动里面增加了一个任务，然后继续向下执行<br>（同步里面有阻塞与非阻塞，阻塞就是睡眠，非阻塞就是空转轮询）</p><p><img src="/images/Pasted%20image%2020260516145200.png"></p><p><img src="/images/Pasted%20image%2020260516145401.png"></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;cuda_runtime.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 检查cudaapi 函数返回值</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CUDA_CHECK(call) do &#123; \</span></span><br><span class="line"><span class="meta">    cudaError_t err = call; \</span></span><br><span class="line"><span class="meta">    <span class="keyword">if</span> (err != cudaSuccess) &#123; \</span></span><br><span class="line"><span class="meta">        fprintf(stderr, <span class="string">&quot;CUDA error at %s:%d: %s\n&quot;</span>, \</span></span><br><span class="line"><span class="meta">                __FILE__, __LINE__, cudaGetErrorString(err)); \</span></span><br><span class="line"><span class="meta">        exit(EXIT_FAILURE); \</span></span><br><span class="line"><span class="meta">    &#125; \</span></span><br><span class="line"><span class="meta">&#125; while(0)</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//核函数，每个thread，计算向量的一位</span></span><br><span class="line">__global__ <span class="type">void</span> <span class="title function_">vector_add</span><span class="params">(<span class="type">const</span> <span class="type">float</span>* a, </span></span><br><span class="line"><span class="params">                            <span class="type">const</span> <span class="type">float</span>* b,</span></span><br><span class="line"><span class="params">                             <span class="type">float</span>* c,</span></span><br><span class="line"><span class="params">                            <span class="type">int</span> n)</span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//获取当前thread id</span></span><br><span class="line">    <span class="type">int</span> idx = blockIdx.x * blockDim.x + threadIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(idx &lt; n)&#123;</span><br><span class="line">        c[idx] = a[idx] + b[idx];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> N = <span class="number">1</span> &lt;&lt; <span class="number">20</span>; <span class="comment">//1M </span></span><br><span class="line">    <span class="type">size_t</span> bytes = N* <span class="keyword">sizeof</span>(<span class="type">float</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//分配主机内存</span></span><br><span class="line">    <span class="type">float</span>* h_a = (<span class="type">float</span>*)<span class="built_in">malloc</span>(bytes);</span><br><span class="line">    <span class="type">float</span>* h_b = (<span class="type">float</span>*)<span class="built_in">malloc</span>(bytes);</span><br><span class="line">    <span class="type">float</span>* h_c = (<span class="type">float</span>*)<span class="built_in">malloc</span>(bytes);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span> ; i&lt;N; i++)&#123;</span><br><span class="line">        h_a[i] = <span class="number">1.0f</span>;</span><br><span class="line">        h_b[i]=<span class="number">1.0f</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//分配显存</span></span><br><span class="line">    <span class="type">float</span>* d_a, *d_b, *d_c;</span><br><span class="line">    CUDA_CHECK(cudaMalloc(&amp;d_a, bytes));</span><br><span class="line">    CUDA_CHECK(cudaMalloc(&amp;d_b, bytes));</span><br><span class="line">    CUDA_CHECK(cudaMalloc(&amp;d_c, bytes));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//拷贝数据</span></span><br><span class="line">    CUDA_CHECK(cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice));</span><br><span class="line">    CUDA_CHECK(cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice));</span><br><span class="line"></span><br><span class="line">    <span class="comment">//开始让cuda计算，开始准备kernel launch, 一共N个thread</span></span><br><span class="line">    <span class="type">int</span> blockSize = <span class="number">256</span>; <span class="comment">//一个block 256 thread</span></span><br><span class="line">    <span class="type">int</span> gridSize = (N+blockSize <span class="number">-1</span>)/ blockSize;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//启动kernel, 所有thread： 0 ~ N-1</span></span><br><span class="line">    vector_add&lt;&lt;&lt;gridSize, blockSize&gt;&gt;&gt;(d_a, d_b, d_c, N);</span><br><span class="line">    CUDA_CHECK(cudaGetLastError());</span><br><span class="line"></span><br><span class="line">    CUDA_CHECK(cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost));</span><br><span class="line"></span><br><span class="line">    <span class="comment">//验证</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i&lt;N; i++)&#123;</span><br><span class="line">        <span class="keyword">if</span>(h_c[i] != <span class="number">3.0f</span>)&#123;</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">&quot;Error calcute in thread idx = %d, %f\n&quot;</span>, i, h_c[i]);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;Vector addition completed successfully\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">free</span>(h_a);</span><br><span class="line">    <span class="built_in">free</span>(h_b);</span><br><span class="line">    <span class="built_in">free</span>(h_c);</span><br><span class="line">    cudaFree(d_a);</span><br><span class="line">    cudaFree(d_b);</span><br><span class="line">    cudaFree(d_c);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="基础算子学习"><a href="#基础算子学习" class="headerlink" title="基础算子学习"></a>基础算子学习</h1><h2 id="softmax算子"><a href="#softmax算子" class="headerlink" title="softmax算子"></a>softmax算子</h2><p><img src="/images/Pasted%20image%2020260520194902.png"></p><h3 id="计算过程分析："><a href="#计算过程分析：" class="headerlink" title="计算过程分析："></a>计算过程分析：</h3><p>首先，所有的数据是放在全局内存里面的，</p><p><strong>以cpu的视角来分析</strong></p><ol><li>我们需要计算这些浮点数的max最大值，因此需要一个遍历O(n)</li><li>我们需要计算每个float经过max的归一化之后的exp, 需要一个遍历O(n)</li><li>我们需要计算分母：所有exp的求和，需要一个遍历O(n)</li><li>我们需要计算所有exp除以求和的分母，得到每个的分数，需要一遍O(n)</li></ol><p><strong>以gpu并行的视角来分析</strong>：</p><ol><li>我们需要计算这些浮点数的max最大值，因此需要一个遍历O(n)<ol><li>可以用几个thread来并行计算，<strong>需要交互数据</strong><ol><li><strong>交互数据：求最大值</strong></li></ol></li></ol></li><li>我们需要计算每个float经过max的归一化之后的exp, 需要一个遍历O(n)<ol><li>可以用几个thread来并行计算，<strong>无需交互数据</strong></li></ol></li><li>我们需要计算分母：所有exp的求和，需要一个遍历O(n)<ol><li>可以用几个thread来并行计算，<strong>需要交互数据</strong><ol><li><strong>交互数据：求和</strong></li></ol></li></ol></li><li>我们需要计算所有exp除以求和的分母，得到每个的分数，需要一遍O(n)<ol><li>可以用几个thread来并行计算，<strong>无需交互数据</strong></li></ol></li></ol><p>所以，这里涉及<strong>两种thread交互数据的需求</strong>：本质都是<strong>thread交互数据</strong>。（<mark style="background:#fff88f">所有thread都属于一个block</mark>）<br>这里由两种办法：</p><ul><li><strong>warp间thread交换数据</strong><ul><li><strong>共享内存</strong></li></ul></li><li><strong>warp内thread交换数据</strong><ul><li><strong>寄存器（更快）</strong><ul><li>__shrf_xxx</li></ul></li><li>共享内存</li></ul></li></ul><p><mark style="background:#fff88f">而这种靠thread交互数据，来得到我们最终的计算结果的方法，叫归约</mark></p><blockquote><p>注意，因为我们的数据很大，所以一般数据都放在全局内存里面</p></blockquote><h3 id="几种thread的遍历方式"><a href="#几种thread的遍历方式" class="headerlink" title="几种thread的遍历方式"></a>几种thread的遍历方式</h3><h4 id="无需交互"><a href="#无需交互" class="headerlink" title="无需交互"></a>无需交互</h4><ul><li>采样处理<ul><li><code>for(int i = tid; i&lt;C ; i += blockDim.x)</code><ul><li>这样就是像周期一样去采样我们的全局内存的数据</li></ul></li></ul></li><li>平分处理<ul><li>就是正常的等分，然后挨个offset处理</li></ul></li></ul><p>因为无需交互，所以每个thread的处理结果一般就<strong>放在寄存器里面</strong></p><h4 id="需要交互（归约）"><a href="#需要交互（归约）" class="headerlink" title="需要交互（归约）"></a>需要交互（归约）</h4><ul><li>warp内（先算）<ul><li>固定32个thread，用__shrf_xxx来warpReduce</li><li>几次循环之后，只有lane0的结果才是warp内归约的最后结果，其他的都是中间结果</li></ul></li><li>warp间<ul><li>共享内存暂存，然后选择一个tid来遍历共享内存。合并warp间的结果</li></ul></li><li>通用归约逻辑：<ul><li>二分选取交互对象</li><li>步长选取交互对象</li></ul></li></ul><h4 id="thread指定范围"><a href="#thread指定范围" class="headerlink" title="thread指定范围"></a>thread指定范围</h4><ul><li>if(tid &#x3D;&#x3D; 0)</li><li>if(tid &lt; xxx)<br>指定某些tid工作，剩余的往下，阻塞在__syncthreads()</li></ul><h3 id="cpu实现"><a href="#cpu实现" class="headerlink" title="cpu实现"></a>cpu实现</h3><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// N 为张量的个数，h为一个张量的维度</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">softmax_cpu</span><span class="params">(<span class="type">float</span>* input, <span class="type">float</span>* output,<span class="type">int</span> N, <span class="type">int</span> C)</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i&lt; N ; i++)&#123;</span><br><span class="line">        <span class="type">const</span> <span class="type">float</span>* input_row = input + i*C; <span class="comment">//计算第i个向量的偏移</span></span><br><span class="line">        <span class="type">float</span>* out_row = output + i*C; <span class="comment">//计算输出的同样位置</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取z的最大值</span></span><br><span class="line">        <span class="type">float</span> maxval = -INFINITY;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j&lt;C;j++)&#123;</span><br><span class="line">            <span class="keyword">if</span>(maxval &lt; input_row[j])</span><br><span class="line">                maxval = input_row[j];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//开始求和</span></span><br><span class="line">        <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j =<span class="number">0</span>; j&lt;C;j++)&#123;</span><br><span class="line">            out_row[j] = <span class="built_in">expf</span>(input_row[j]-maxval);</span><br><span class="line">            sum += out_row[j];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//处理每个分子</span></span><br><span class="line">        <span class="type">float</span> norm = <span class="number">1.0f</span>/sum;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j&lt;C; j++)&#123;</span><br><span class="line">            out_row[j] *= norm;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="cuda朴素实现"><a href="#cuda朴素实现" class="headerlink" title="cuda朴素实现"></a>cuda朴素实现</h3><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//朴素实现</span></span><br><span class="line"><span class="comment">//每行input向量，使用一个block, 每个block仅包含1个thread</span></span><br><span class="line"><span class="comment">//相当于小批量并行</span></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">缺点：</span></span><br><span class="line"><span class="comment">    shared_memory没有用到，利用率低</span></span><br><span class="line"><span class="comment">    显存利用率也低，</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">softmax_gpu_native</span><span class="params">(<span class="type">float</span>* input_gpu, <span class="type">float</span>* output_gpu, <span class="type">int</span> N, <span class="type">int</span> C)</span></span>&#123;</span><br><span class="line">    <span class="comment">//获取当前的thread的grid全局编号i，block内编号threadIdx.x</span></span><br><span class="line">    <span class="type">int</span> i = threadIdx.x + blockIdx.x * blockDim.x;</span><br><span class="line">    <span class="comment">//printf(&quot;i = %d\n&quot;, i);</span></span><br><span class="line">    <span class="keyword">if</span>(i&lt;N)&#123;</span><br><span class="line">        <span class="type">const</span> <span class="type">float</span> * input_row = input_gpu + C*i;</span><br><span class="line">        <span class="type">float</span>* output_row = output_gpu + C*i;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//maxval</span></span><br><span class="line">        <span class="type">float</span> maxval = -INFINITY;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n&lt;C ; n++)&#123;</span><br><span class="line">            <span class="keyword">if</span>(maxval &lt; input_row[n])</span><br><span class="line">                maxval = input_row[n];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//sum</span></span><br><span class="line">        <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j&lt;C ;j++)&#123;</span><br><span class="line">            <span class="type">float</span> val = <span class="built_in">expf</span>(input_row[j] - maxval);</span><br><span class="line">            output_row[j] = val;</span><br><span class="line">            sum += val;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">float</span> norm = <span class="number">1</span>/sum;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j&lt;C;j++)&#123;</span><br><span class="line">            output_row[j] *= norm;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>cpu cost time &#x3D; 568.371 ms<br>Results match: YES<br>softmax_native time: 196.05434 ms</p><h3 id="优化1：block内多thread归约-共享内存"><a href="#优化1：block内多thread归约-共享内存" class="headerlink" title="优化1：block内多thread归约+共享内存"></a>优化1：block内多thread归约+共享内存</h3><p><strong>核函数</strong>里面，写的线程行为，分为<strong>两种作用范围：</strong></p><ul><li>特定范围的thread</li><li>全体thread</li></ul><p><strong>归约</strong>，从形式上来看，有两种循环：</p><ul><li>抽样周期归约：<code>for(int i = tid; i &lt; C; i+=block_size){</code></li><li>等分归约：<code>for(int j = 0; j&lt;C;j++){</code></li></ul><p><strong>共享内存</strong>里面，主要放归约之后的结果：</p><ul><li>找到的最值</li><li>求的和</li></ul><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 改进1：共享内存 + 块内规约</span></span><br><span class="line"><span class="comment"> * 1. 块内也要划分多个thread：规约</span></span><br><span class="line"><span class="comment"> * 2. 块内用共享内存来储存计算中间值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">softmax_v1</span><span class="params">(<span class="type">float</span>* input_gpu, <span class="type">float</span>* output_gpu, <span class="type">int</span> N, <span class="type">int</span> C)</span></span>&#123;</span><br><span class="line">    <span class="keyword">extern</span> __shared__ <span class="type">float</span> smem[];</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> idx = blockIdx.x; <span class="comment">//该thread是第几个block</span></span><br><span class="line">    <span class="type">int</span> tid = threadIdx.x; <span class="comment">//该thread的block内id</span></span><br><span class="line">    <span class="type">int</span> block_size = blockDim.x; <span class="comment">//一个block有多少个thread</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">float</span>* x = input_gpu + idx*C; <span class="comment">//该block的thread共同处理的一个向量</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//归约找到最大值（抽样求和归约）</span></span><br><span class="line">    <span class="type">float</span> maxval = -INFINITY;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i &lt; C; i+=block_size)&#123;</span><br><span class="line">        maxval = <span class="built_in">fmaxf</span>(maxval, x[i]);</span><br><span class="line">    &#125;</span><br><span class="line">    smem[tid] = maxval;</span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> stride = block_size / <span class="number">2</span>; stride &gt;= <span class="number">1</span>; stride /=<span class="number">2</span>)&#123;</span><br><span class="line">        <span class="comment">//每次指定当前thread数量的一半的thread，来二分归约出max</span></span><br><span class="line">        __syncthreads();</span><br><span class="line">        <span class="keyword">if</span>(tid&lt;stride)&#123;</span><br><span class="line">            smem[tid] = <span class="built_in">fmaxf</span>(smem[tid], smem[tid + stride]);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    __syncthreads();</span><br><span class="line">    <span class="comment">//此时smem[0]就是最大值, 保存到寄存器区</span></span><br><span class="line">    <span class="type">float</span> offset = smem[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//求和算分母</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i+=block_size)&#123;</span><br><span class="line">        output_gpu[idx*C + i] = <span class="built_in">expf</span>(x[i] - offset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//下面又要归约计算了</span></span><br><span class="line">    __syncthreads(); <span class="comment">//先同步一下</span></span><br><span class="line"></span><br><span class="line">    <span class="type">float</span>* y = output_gpu + idx*C;</span><br><span class="line">    <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//每个thread</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C;i+=block_size)&#123;</span><br><span class="line">        sum += y[i];</span><br><span class="line">    &#125;</span><br><span class="line">    smem[tid]=sum;</span><br><span class="line">    __syncthreads(); <span class="comment">//再同步一下</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> stride = block_size /<span class="number">2</span> ; stride &gt;=<span class="number">1</span>; stride /=<span class="number">2</span>)&#123;</span><br><span class="line">        __syncthreads(); <span class="comment">//再同步一下</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//特定thread</span></span><br><span class="line">        <span class="keyword">if</span>(tid &lt; stride)</span><br><span class="line">            smem[tid] += smem[tid+stride];</span><br><span class="line">    &#125;</span><br><span class="line">    __syncthreads(); <span class="comment">//再同步一下</span></span><br><span class="line">    </span><br><span class="line">    <span class="type">float</span> sum_2 = smem[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//全体thread</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i+=block_size)&#123;</span><br><span class="line">        output_gpu[idx*C+i] = y[i]/sum_2;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>cpu cost time &#x3D; 568.371 ms<br>Results match: YES<br>softmax_native time: 8.05434 ms</p><h3 id="优化2-warp内归约-共享内存"><a href="#优化2-warp内归约-共享内存" class="headerlink" title="优化2: warp内归约+共享内存"></a>优化2: warp内归约+共享内存</h3><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 改进2：warp束内归约</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">__device__ <span class="type">float</span> <span class="title">warpReduceMax</span><span class="params">(<span class="type">float</span> val)</span></span>&#123;</span><br><span class="line">    <span class="comment">//每个线程做2分归约，一直2分到最后一个</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> offset = <span class="number">16</span>; offset&gt;<span class="number">0</span>;offset /=<span class="number">2</span>)&#123;</span><br><span class="line">        <span class="comment">//每轮归约，每个线程获取高offset的thread的val数值，就是自己当前的val的比较值，由</span></span><br><span class="line">        <span class="comment">//每个thread进入warpReduceMax时传入的值val为准</span></span><br><span class="line">        val = <span class="built_in">fmaxf</span>(val, __shfl_down_sync(<span class="number">0xFFFFFFFF</span>, val, offset));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> val;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">__device__ <span class="type">float</span> <span class="title">warpReduceSum</span><span class="params">(<span class="type">float</span> val)</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> offset = <span class="number">16</span>; offset &gt; <span class="number">0</span> ; offset /= <span class="number">2</span>)&#123;</span><br><span class="line">        val = val + __shfl_down_sync(<span class="number">0xFFFFFFFF</span>, val, offset);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> val;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">softmax_v2</span><span class="params">(<span class="type">float</span>* input_gpu, <span class="type">float</span>* output_gpu, <span class="type">int</span> N, <span class="type">int</span> C)</span></span>&#123;</span><br><span class="line">    <span class="keyword">extern</span> __shared__ <span class="type">float</span> shared[]; <span class="comment">// shared[2 * warpsPerBlock]</span></span><br><span class="line">    <span class="type">int</span> bid = blockIdx.x;</span><br><span class="line">    <span class="type">int</span> tid = threadIdx.x;</span><br><span class="line">    <span class="type">int</span> warpId = threadIdx.x / <span class="number">32</span>;</span><br><span class="line">    <span class="type">int</span> lanId = threadIdx.x % <span class="number">32</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> warpsPerBlock = blockDim.x / <span class="number">32</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">float</span>* maxvals = shared;<span class="comment">//前半段放每个warp的最大值</span></span><br><span class="line">    <span class="type">float</span>* sumvals = &amp;shared[warpsPerBlock];<span class="comment">//后半段放求和</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">float</span>* x = input_gpu + bid*C; <span class="comment">//找到这个block对应的C</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//最大值： 全局内存-&gt;共享内存</span></span><br><span class="line">    <span class="type">float</span> maxval = -INFINITY;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i += blockDim.x)&#123;</span><br><span class="line">        maxval = <span class="built_in">fmaxf</span>(maxval, x[i]); </span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//寄存器maxval = 每个thread的max</span></span><br><span class="line">    maxval = <span class="built_in">warpReduceMax</span>(maxval); <span class="comment">//一个warp中的最大值</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(lanId == <span class="number">0</span>)&#123;</span><br><span class="line">        maxvals[warpId] = maxval;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads(); <span class="comment">//同步等一下所有的thread完成</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(tid == <span class="number">0</span>)&#123;</span><br><span class="line">        <span class="type">float</span> val = maxvals[tid];</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">1</span>; i&lt;warpsPerBlock; i++)&#123;</span><br><span class="line">            val = <span class="built_in">fmaxf</span>(val, maxvals[i]);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        maxvals[<span class="number">0</span>] = val;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//计算分子/分母局部</span></span><br><span class="line">    <span class="type">float</span>* y = output_gpu + bid*C;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i+=blockDim.x)&#123;</span><br><span class="line">        y[i] = <span class="built_in">expf</span>(x[i] - maxvals[<span class="number">0</span>]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//下面开始求和</span></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i+=blockDim.x)&#123;</span><br><span class="line">        sum += y[i];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    sum = <span class="built_in">warpReduceSum</span>(sum);</span><br><span class="line">    <span class="keyword">if</span>(lanId == <span class="number">0</span>)&#123;</span><br><span class="line">        sumvals[warpId] = sum;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(tid == <span class="number">0</span>)&#123;</span><br><span class="line">        <span class="type">float</span> sum_all = sumvals[<span class="number">0</span>];</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> i =<span class="number">1</span>; i&lt;warpsPerBlock; i++)&#123;</span><br><span class="line">            sum_all += sumvals[i];</span><br><span class="line">        &#125;</span><br><span class="line">        sumvals[<span class="number">0</span>] = sum_all;</span><br><span class="line">    &#125;</span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 和也写入了sumvals[0]了</span></span><br><span class="line"></span><br><span class="line">    <span class="type">float</span> norm = <span class="number">1</span>/sumvals[<span class="number">0</span>];</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = tid; i&lt;C; i += blockDim.x)&#123;</span><br><span class="line">        y[i] *= norm;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>cpu cost time &#x3D; 571.986 ms<br>Results match: YES<br>softmax_v2 time: 5.70531 ms</p><h3 id="总结：优化手段："><a href="#总结：优化手段：" class="headerlink" title="总结：优化手段："></a>总结：优化手段：</h3><ol><li>小批量并行<ol><li>一个block针对一个样本（向量）</li></ol></li><li>一个block内的thread数为32的倍数，凑够warp来调度。</li><li>warp内归约（寄存器归约）</li><li>warp间合并（共享内存）</li></ol><h2 id="Reduce算子"><a href="#Reduce算子" class="headerlink" title="Reduce算子"></a>Reduce算子</h2><p>这里就是把上面softmax的归约方式进一步总结</p><p>前面讲了，我们的归约，本质就是<strong>thread的交互计算</strong>。</p><p>本质就是<strong>利用数学的结合率</strong>，来把<strong>线性执行</strong>转化为<strong>树状执行</strong>。</p><p><img src="/images/Pasted%20image%2020260522163802.png" alt="383"></p><p>总的来说，归约有三种方式</p><ol><li><strong>跨线程块归约</strong>：如何将多个线程块（CTA）的局部结果进一步聚合为最终结果；</li><li><strong>warp 内归约</strong>：利用 warp-level 原语（如 <code>__shfl_down_sync</code>）实现高效的 warp 内部数据聚合；</li><li><strong>单 warp 循环展开策略</strong>：通过循环展开提升指令级并行性（ILP），优化计算吞吐和内存访问效率。</li></ol><p>我们之前softmax用到的是第二种归约方式。</p><h3 id="朴素归约reduce实现-分块归约"><a href="#朴素归约reduce实现-分块归约" class="headerlink" title="朴素归约reduce实现(分块归约)"></a>朴素归约reduce实现(分块归约)</h3><p><img src="/images/675e0ba0ab52c669f5eb1120f68eb488.jpg"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;cuda_runtime.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;chrono&gt;</span>  <span class="comment">// 用于 CPU 计时</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;numeric&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;vector&gt;</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> BLOCK_SIZE = <span class="number">1024</span>;</span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1024</span> * <span class="number">1024</span>;  <span class="comment">// 1M elements //float序列的长度</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">reduce_v0</span><span class="params">(<span class="type">float</span>* g_idata, <span class="type">float</span> *g_odata)</span></span>&#123;</span><br><span class="line">    __shared__ <span class="type">float</span> sdata[BLOCK_SIZE];</span><br><span class="line"></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> tid = threadIdx.x;<span class="comment">//block内thread索引</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> i = blockIdx.x * blockDim.x + threadIdx.x; <span class="comment">//全局thread索引</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//把全局内存的数据，并行拷贝到共享内存里面</span></span><br><span class="line">    <span class="comment">//注意，每个block都有各自的共享内存，所以不会覆盖，</span></span><br><span class="line">    <span class="comment">//该指令会把全局内存中的数据，全部拷贝的到所有block的对应的共享内存中</span></span><br><span class="line">    <span class="comment">//所以是多个block对应一个序列的情况, 每个thread对应一个float</span></span><br><span class="line">    sdata[tid] = g_idata[i];</span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//s = 步长</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">unsigned</span> <span class="type">int</span> s = <span class="number">1</span>; s&lt;blockDim.x; s*=<span class="number">2</span>)&#123;</span><br><span class="line">        <span class="keyword">if</span>(tid%(<span class="number">2</span>*s) == <span class="number">0</span>)</span><br><span class="line">            sdata[tid] += sdata[tid+s];</span><br><span class="line"></span><br><span class="line">        <span class="comment">//因为是异步的，所以每轮都需要同步后，再归约下一轮</span></span><br><span class="line">        __syncthreads();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(tid == <span class="number">0</span>)&#123;</span><br><span class="line">        <span class="comment">//每个block的第一个thread，负责把共享内存中每个block的块内归约的结果，写回全局显存</span></span><br><span class="line">        g_odata[blockIdx.x] = sdata[<span class="number">0</span>];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// CPU验证函数</span></span><br><span class="line"><span class="function"><span class="type">float</span> <span class="title">reduce_cpu</span><span class="params">(<span class="type">const</span> std::vector&lt;<span class="type">float</span>&gt; &amp;data)</span> </span>&#123;</span><br><span class="line">  <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">float</span> val : data) &#123;</span><br><span class="line">    sum += val;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="type">int</span> num_blocks = (N + BLOCK_SIZE - <span class="number">1</span>) / BLOCK_SIZE;</span><br><span class="line"></span><br><span class="line">  <span class="function">std::vector&lt;<span class="type">float</span>&gt; <span class="title">h_data</span><span class="params">(N)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; N; i++) &#123;</span><br><span class="line">    h_data[i] = <span class="number">1.0f</span>;  <span class="comment">// 简单起见，全部初始化为1.0</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line">  <span class="comment">// CPU 计时开始</span></span><br><span class="line">  <span class="keyword">auto</span> cpu_start = std::chrono::high_resolution_clock::<span class="built_in">now</span>();</span><br><span class="line"></span><br><span class="line">  <span class="type">float</span> cpu_result = <span class="built_in">reduce_cpu</span>(h_data);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> cpu_end = std::chrono::high_resolution_clock::<span class="built_in">now</span>();</span><br><span class="line">  std::chrono::duration&lt;<span class="type">double</span>, std::milli&gt; cpu_duration = cpu_end - cpu_start;</span><br><span class="line">  <span class="comment">// CPU 计时结束</span></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line"></span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;CPU result: &quot;</span> &lt;&lt; cpu_result &lt;&lt; std::endl;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;CPU time: &quot;</span> &lt;&lt; cpu_duration.<span class="built_in">count</span>() &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; std::endl;</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 class="type">float</span> *d_data, *d_result;</span><br><span class="line">  <span class="type">float</span> *d_final_result;</span><br><span class="line">  <span class="type">float</span> gpu_result;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_data, N * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_result, num_blocks * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_final_result, <span class="number">1</span> * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMemcpy</span>(d_data, h_data.<span class="built_in">data</span>(), N * <span class="built_in">sizeof</span>(<span class="type">float</span>), cudaMemcpyHostToDevice);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line">  <span class="comment">// GPU 计时开始 (CUDA Events)</span></span><br><span class="line">  cudaEvent_t start, stop;</span><br><span class="line">  <span class="built_in">cudaEventCreate</span>(&amp;start);</span><br><span class="line">  <span class="built_in">cudaEventCreate</span>(&amp;stop);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventRecord</span>(start);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//第一个返回的是每个块内归约的结果，一共num_blocks个float</span></span><br><span class="line">  reduce_v0&lt;&lt;&lt;num_blocks, BLOCK_SIZE&gt;&gt;&gt;(d_data, d_result);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//再次启动一个block，针对num_blocks个float的thread的块内归约</span></span><br><span class="line">  reduce_v0&lt;&lt;&lt;<span class="number">1</span>, num_blocks&gt;&gt;&gt;(d_result, d_final_result);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventRecord</span>(stop);</span><br><span class="line">  <span class="built_in">cudaEventSynchronize</span>(stop);</span><br><span class="line"></span><br><span class="line">  <span class="type">float</span> milliseconds = <span class="number">0</span>;</span><br><span class="line">  <span class="built_in">cudaEventElapsedTime</span>(&amp;milliseconds, start, stop);</span><br><span class="line">  <span class="comment">// GPU 计时结束</span></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line"></span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;GPU kernel time: &quot;</span> &lt;&lt; milliseconds &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; std::endl;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMemcpy</span>(&amp;gpu_result, d_final_result, <span class="built_in">sizeof</span>(<span class="type">float</span>),</span><br><span class="line">             cudaMemcpyDeviceToHost);</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;GPU result: &quot;</span> &lt;&lt; gpu_result &lt;&lt; std::endl;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">abs</span>(cpu_result - gpu_result) &lt; <span class="number">1e-5</span>) &#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Result verified successfully!&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Result verification failed!&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 清理资源</span></span><br><span class="line">  <span class="built_in">cudaFree</span>(d_data);</span><br><span class="line">  <span class="built_in">cudaFree</span>(d_result);</span><br><span class="line">  <span class="built_in">cudaFree</span>(d_final_result);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventDestroy</span>(start);</span><br><span class="line">  <span class="built_in">cudaEventDestroy</span>(stop);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">CPU result: 1.04858e+06</span><br><span class="line">CPU time: 4.79212 ms</span><br><span class="line">GPU kernel time: 0.101632 ms</span><br><span class="line">GPU result: 1.04858e+06</span><br><span class="line">Result verified successfully!</span><br></pre></td></tr></table></figure><p>从上述 CUDA 实现可以看出，该归约核函数采用的是分块归约（block-wise reduction）策略：每个线程块独立处理输入数据的一个子集，并将该子集的归约结果写入输出数组 <code>g_odata[blockIdx.x]</code></p><p><img src="/images/Pasted%20image%2020260522174902.png"></p><h4 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h4><p>从 Nsight Compute 的分析结果可以看出，<strong>计算效率</strong>和<strong>访存效率</strong>双低，均不到40%。</p><p>先解释一下各个<strong>性能标准</strong></p><ul><li><mark style="background:#fff88f">全局内存带宽利用率</mark><ul><li><strong>是什么</strong>：实际带宽 &#x2F; GPU 理论峰值带宽 × 100%</li><li><strong>为什么关键</strong>：归约是典型的访存密集型操作，算术强度（~0.25 FLOP&#x2F;Byte）极低，瓶颈全在带宽上</li><li><strong>如何衡量</strong>：<code>（读 + 写的字节数）/ kernel 执行时间</code>，和 <code>cudaMemcpy</code> 的纯带宽对比</li><li><strong>优化方向</strong>：合并访问、减少访问次数（每线程 load 多个元素）、对齐</li></ul></li><li><mark style="background:#fff88f">合并访问</mark><ul><li><strong>是什么</strong>：一个 warp 的 32 个线程访问的全局内存地址落在同一个 128-byte cache line 内</li><li><strong>你的代码</strong>：<code>g_idata[blockIdx.x * 1024 + threadIdx.x]</code> —— 相邻线程访问相邻地址，已经合并了</li><li><strong>如果不合并</strong>：比如 stride 访问，一次 transaction 只能取回 1&#x2F;32 的数据，带宽利用率暴跌</li></ul></li><li><mark style="background:#fff88f">Occupancy(SM占用率)</mark><ul><li><strong>是什么</strong>：每个 SM 上活跃 warp 数 &#x2F; 理论最大 warp 数</li><li><strong>公式</strong>：<code>active_warps = min(blocks * threads/32, 寄存器限制, 共享内存限制)</code></li><li><strong>为什么重要</strong>：活跃 warp 越多，SM 越有机会在等待访存时切换到其他 warp，隐藏延迟</li><li><strong>你的代码</strong>：1024 线程&#x2F;block，每个线程只用一个 float 共享内存（4KB&#x2F;block），寄存器也很少，occupancy 其实不低</li></ul></li><li><mark style="background:#fff88f">Warp Divergence(Warp分支分化)</mark>（<strong>warp内活跃thread数占比</strong>）<ul><li><strong>是什么</strong>：warp 内 32 个线程走了 if&#x2F;else 不同分支，导致部分线程闲置</li><li><strong>你的代码</strong>：<code>if(tid % (2*s) == 0)</code> —— 越往后越惨，最后只有 1&#x2F;32 的线程在干活</li><li><strong>衡量</strong>：SIMD 效率 &#x3D; 活跃线程数 &#x2F; 32</li><li><strong>优化</strong>：用跨步递减（<code>s &gt;&gt;= 1</code>）替代跨步递增，让活跃线程集中在 warp 前半部</li></ul></li><li><mark style="background:#fff88f">共享内存Bank conflict</mark><ul><li><strong>是什么</strong>：shared memory 有 32 个 bank（每个 4 bytes），同一 warp 内 ≥2 个线程访问同一 bank 的不同地址时串行化</li><li><strong>你的代码</strong>：当 <code>s &gt;= 32</code> 时，<code>sdata[tid]</code> 和 <code>sdata[tid+s]</code> 可能落在同一 bank</li><li><strong>N-way bank conflict</strong>：最坏情况下 32 个线程全撞同一 bank，访问变成串行，延迟 ×32</li><li><strong>优化</strong>：sequential addressing（连续地址访问）或 padding</li></ul></li><li><mark style="background:#fff88f">算术强度</mark><ul><li><strong>公式</strong>：FLOP &#x2F; Byte</li><li><strong>归约</strong>：N 个元素做 N-1 次加法，读 N 个 float，写 1 个 float，约 <code>N / (4N + 4) ≈ 0.25 FLOP/Byte</code></li><li><strong>含义</strong>：每读 1 个字节只做 0.25 次浮点运算，GPU 算力再强也使不上劲，纯内存瓶颈</li></ul></li><li><mark style="background:#fff88f">指令效率</mark><ul><li><code>%</code>（取模）、<code>/</code>（除法）在 GPU 上比 <code>+ - *</code> 慢 <strong>10~20 倍</strong></li><li>浮点加法的吞吐通常为几十到上百个&#x2F;时钟&#x2F;SM</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260522180724.png"></p><p><mark style="background:#affad1">计算效率：</mark></p><ul><li>SM利用率<ul><li>SM有多少时间真正在跑指令&#x2F;等访存，同步，空闲<ul><li>__syncthreads(), 取模，会导致SM利用率低</li></ul></li></ul></li><li>IPC（每SM的每时钟执行几条指令）<ul><li>理论峰值取决于GPU架构（4-8）</li><li>warp stall, 等访存，等barrier, 等指令依赖，会降低IPC</li></ul></li><li>Warp内活跃thread占比</li></ul><p><img src="/images/Pasted%20image%2020260522181050.png" alt="349"></p><ul><li>指令吞吐<ul><li>取模指令会被编译成一长串整数指令序列，特别是对非 2 的幂取模</li><li>对 1024 线程来说，这 10 轮迭代就是 10000+ 次取模</li></ul></li><li>控制流开销<ul><li><code>__syncthreads()</code> 本身有开销（barrier 同步）</li><li>你的代码在循环内 10 次 <code>__syncthreads()</code>，每次都要等最慢的那个 warp</li></ul></li></ul><p><mark style="background:#affad1">访存效率</mark></p><ul><li>全局内存带宽利用率<ul><li>每个thread读的数据不多，导致每次访存明明最多可以拿128字节，但是你就拿了32字节，跑不满</li></ul></li><li>L1&#x2F;L2 cache 缓存命中率<ul><li><img src="/images/Pasted%20image%2020260522181350.png" alt="278"></li></ul></li><li>全局内存 Access Pattern &#x2F; Transaction 效率<ul><li><strong>指标</strong>：<code>gld_efficiency</code>（global load efficiency）、<code>gst_efficiency</code>（global store efficiency）</li><li>实际用到的字节 &#x2F; 实际传输的字节<ul><li><strong>你的代码</strong>：<code>g_idata[blockIdx.x * 1024 + threadIdx.x]</code> 是顺序访问，一个 warp（32线程）读 128 bytes，刚好是一个 cache line，gld_efficiency 接近 100%</li><li>你写的 <code>g_odata[blockIdx.x]</code> 每个 block 只写 4 bytes——而 GPU 一次最少写 32 bytes，浪费了 28 bytes，<code>gst_efficiency</code> 很差</li></ul></li></ul></li><li>共享内存 bank conflict<ul><li><strong>指标</strong>：<code>shared_load_transactions</code> &#x2F; <code>shared_store_transactions</code>（越多冲突，transaction 越多）</li><li><strong>32 个 bank，每个 4 bytes</strong>：同一 warp 内两个线程访问同一 bank 的不同地址 → 一次变两次传输</li><li>你的代码 <code>s=32</code> 时，thread 0 读 bank 0 (<code>sdata[0]</code>) 和 bank 0 (<code>sdata[32]</code>) → 2-way conflict</li><li>一个bank读一个float, 一次读最多32个float，否则就会有bank conflict</li></ul></li><li>访存延迟隐藏<ul><li><strong>原理</strong>：SM 在等一个 warp 的数据时，立刻切到另一个已就绪的 warp 继续干活</li><li><strong>关键</strong>：活跃 warp 越多，越能掩盖 ~600 cycles 的 HBM 延迟</li><li><strong>你的代码</strong>：occupancy 不低，但 <code>__syncthreads()</code> 把所有 warp 都卡在 barrier，硬件没法切换，访存延迟完全暴露</li></ul></li></ul><p>因此，本朴素实现，<mark style="background:#affad1">可能优化的点有</mark>：</p><ol><li><strong>线程闲置</strong>（从block的总体的thread来看）（<strong>活跃thread总数越来越少</strong>）<ol><li>因为采用两两归约方式，从最开始block所有thread&#x2F;2, 后面每次减半</li><li><strong>总的来说就是参与计算的线程数过少。</strong></li></ol></li><li>warp <strong>线程束分化</strong>，导致warp的效率低（<strong>每个warp内的活跃线程越少</strong>）<ol><li>导致warp效率不高<img src="/images/Pasted%20image%2020260522210526.png"><img src="/images/Pasted%20image%2020260522210541.png"></li><li><img src="/images/94905b858d7ddf417b1a792c43a2ed88.jpg"></li></ol></li><li><strong>存储体冲突</strong>（bank conflict）<ol><li><strong>因为活跃线程间隔分散，导致不同warp的同位置活跃</strong><br> warp&#x3D;32个thread，当步长超过32后，还容易出现共享内存bankconflict</li></ol></li></ol><p><img src="/images/Pasted%20image%2020260522210908.png"><br>4. <strong>循环展开最后一个warp</strong><br><img src="/images/675e0ba0ab52c669f5eb1120f68eb488.jpg"><br><strong>当逐渐归约到只剩下编号在0-31的thread之后，其实就只剩下一个warp在SM里面执行了。</strong></p><p>此时，这些线程在硬件层面是以 <strong>SIMT（Single Instruction, Multiple Thread）</strong> 模式同步执行的</p><p>所以此时，已经<strong>没有必要</strong>再调用__syncthreads()来进行同步了。</p><p>在这个阶段，也没必要条件判断来筛选计算的线程了，<strong>直接算得了</strong>，没必要两两归约了</p><p>所以在归约的循环的后期，（tid &lt; 32）对归约操作进行手动展开，直接执行对应的加法操作，从而消除分支判断和同步开销。<br><img src="/images/Pasted%20image%2020260522211537.png"></p><h3 id="优化1：线程闲置（活跃越来越少）"><a href="#优化1：线程闲置（活跃越来越少）" class="headerlink" title="优化1：线程闲置（活跃越来越少）"></a>优化1：线程闲置（活跃越来越少）</h3><p>策略：在归约循环前增加一次额外的<strong>预归约</strong>操作。<br><img src="/images/Pasted%20image%2020260522212804.png"></p><p><img src="/images/Pasted%20image%2020260522212512.png" alt="341"><br><img src="/images/Pasted%20image%2020260522212532.png"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;cuda_runtime.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;chrono&gt;</span>  <span class="comment">// 用于 CPU 计时</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;numeric&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;vector&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> BLOCK_SIZE = <span class="number">1024</span>;</span><br><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1024</span> * <span class="number">1024</span>;  <span class="comment">// 1M elements //float序列的长度</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//一个block内有1024个thread, 一个thread预处理两个float，所以仅需512个block</span></span><br><span class="line"><span class="comment">//归约的时候，每个thread的归约值，还是存放在共享内存，因为还涉及warp之间</span></span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">reduce_v1</span><span class="params">(<span class="type">float</span>* g_idata, <span class="type">float</span>* g_odata)</span></span>&#123;</span><br><span class="line">    __shared__ <span class="type">float</span> sdata[BLOCK_SIZE];</span><br><span class="line"></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> tid = threadIdx.x;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> i = blockIdx.x * blockDim.x*<span class="number">2</span> + threadIdx.x; <span class="comment">//g_idata访问起始</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">//全体thread</span></span><br><span class="line">    <span class="comment">//预处理</span></span><br><span class="line">    sdata[tid] = g_idata[i] + g_idata[i + blockDim.x];</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//归约</span></span><br><span class="line">    <span class="comment">//不断只剩一半的活跃线程</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">1</span>; s&lt;blockDim.x;s*=<span class="number">2</span>)&#123;</span><br><span class="line">        <span class="keyword">if</span>(tid % (<span class="number">2</span>*s) == <span class="number">0</span>)&#123;</span><br><span class="line">            sdata[tid] += sdata[tid + s];</span><br><span class="line">        &#125;</span><br><span class="line">        __syncthreads();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(tid == <span class="number">0</span>)</span><br><span class="line">        g_odata[blockIdx.x] = sdata[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">&#125;</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 class="comment">// CPU验证函数</span></span><br><span class="line"><span class="function"><span class="type">float</span> <span class="title">reduce_cpu</span><span class="params">(<span class="type">const</span> std::vector&lt;<span class="type">float</span>&gt; &amp;data)</span> </span>&#123;</span><br><span class="line">  <span class="type">float</span> sum = <span class="number">0.0f</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">float</span> val : data) &#123;</span><br><span class="line">    sum += val;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="type">int</span> num_blocks = ((N + BLOCK_SIZE - <span class="number">1</span>) / BLOCK_SIZE)/<span class="number">2</span>;</span><br><span class="line">  cout &lt;&lt; <span class="string">&quot;num_blocks = &quot;</span> &lt;&lt;num_blocks &lt;&lt;endl;</span><br><span class="line">  <span class="function">std::vector&lt;<span class="type">float</span>&gt; <span class="title">h_data</span><span class="params">(N)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; N; i++) &#123;</span><br><span class="line">    h_data[i] = <span class="number">1.0f</span>;  <span class="comment">// 简单起见，全部初始化为1.0</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line">  <span class="comment">// CPU 计时开始</span></span><br><span class="line">  <span class="keyword">auto</span> cpu_start = std::chrono::high_resolution_clock::<span class="built_in">now</span>();</span><br><span class="line"></span><br><span class="line">  <span class="type">float</span> cpu_result = <span class="built_in">reduce_cpu</span>(h_data);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> cpu_end = std::chrono::high_resolution_clock::<span class="built_in">now</span>();</span><br><span class="line">  std::chrono::duration&lt;<span class="type">double</span>, std::milli&gt; cpu_duration = cpu_end - cpu_start;</span><br><span class="line">  <span class="comment">// CPU 计时结束</span></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line"></span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;CPU result: &quot;</span> &lt;&lt; cpu_result &lt;&lt; std::endl;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;CPU time: &quot;</span> &lt;&lt; cpu_duration.<span class="built_in">count</span>() &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; std::endl;</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 class="type">float</span> *d_data, *d_result;</span><br><span class="line">  <span class="type">float</span> *d_final_result;</span><br><span class="line">  <span class="type">float</span> gpu_result;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_data, N * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_result, num_blocks * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line">  <span class="built_in">cudaMalloc</span>(&amp;d_final_result, <span class="number">1</span> * <span class="built_in">sizeof</span>(<span class="type">float</span>));</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMemcpy</span>(d_data, h_data.<span class="built_in">data</span>(), N * <span class="built_in">sizeof</span>(<span class="type">float</span>), cudaMemcpyHostToDevice);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line">  <span class="comment">// GPU 计时开始 (CUDA Events)</span></span><br><span class="line">  cudaEvent_t start, stop;</span><br><span class="line">  <span class="built_in">cudaEventCreate</span>(&amp;start);</span><br><span class="line">  <span class="built_in">cudaEventCreate</span>(&amp;stop);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventRecord</span>(start);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//第一个返回的是每个块内归约的结果，一共num_blocks个float</span></span><br><span class="line">  reduce_v1&lt;&lt;&lt;num_blocks, BLOCK_SIZE&gt;&gt;&gt;(d_data, d_result);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//再次启动一个block，针对num_blocks个float的thread的块内归约</span></span><br><span class="line">  reduce_v1&lt;&lt;&lt;<span class="number">1</span>, num_blocks/<span class="number">2</span>&gt;&gt;&gt;(d_result, d_final_result);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventRecord</span>(stop);</span><br><span class="line">  <span class="built_in">cudaEventSynchronize</span>(stop);</span><br><span class="line"></span><br><span class="line">  <span class="type">float</span> milliseconds = <span class="number">0</span>;</span><br><span class="line">  <span class="built_in">cudaEventElapsedTime</span>(&amp;milliseconds, start, stop);</span><br><span class="line">  <span class="comment">// GPU 计时结束</span></span><br><span class="line">  <span class="comment">// -------------------------------</span></span><br><span class="line"></span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;GPU kernel time: &quot;</span> &lt;&lt; milliseconds &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; std::endl;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaMemcpy</span>(&amp;gpu_result, d_final_result, <span class="built_in">sizeof</span>(<span class="type">float</span>),</span><br><span class="line">             cudaMemcpyDeviceToHost);</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;GPU result: &quot;</span> &lt;&lt; gpu_result &lt;&lt; std::endl;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">abs</span>(cpu_result - gpu_result) &lt; <span class="number">1e-5</span>) &#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Result verified successfully!&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Result verification failed!&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 清理资源</span></span><br><span class="line">  <span class="built_in">cudaFree</span>(d_data);</span><br><span class="line">  <span class="built_in">cudaFree</span>(d_result);</span><br><span class="line">  <span class="built_in">cudaFree</span>(d_final_result);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">cudaEventDestroy</span>(start);</span><br><span class="line">  <span class="built_in">cudaEventDestroy</span>(stop);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">num_blocks = 512</span><br><span class="line">CPU result: 1.04858e+06</span><br><span class="line">CPU time: 2.79929 ms</span><br><span class="line">GPU kernel time: 0.054144 ms</span><br><span class="line">GPU result: 1.04858e+06</span><br><span class="line">Result verified successfully!</span><br></pre></td></tr></table></figure><p>此时有两点改进：</p><ul><li>显存带宽利用率显著提升：<ul><li>每个block的thread都被利用了，读取全局内存+相加+写入寄存器同一条流水线，消除空泡</li><li>每个thread每次读两个float，读取效率翻倍</li></ul></li></ul><blockquote><p>但是此时，线程束分化的问题还是存在。</p></blockquote><h3 id="优化2：线程束分化（活跃thread分散）-bank-conflict"><a href="#优化2：线程束分化（活跃thread分散）-bank-conflict" class="headerlink" title="优化2：线程束分化（活跃thread分散）+ bank conflict"></a>优化2：线程束分化（活跃thread分散）+ bank conflict</h3><p>解决办法就是越来越集中。</p><p>原来是相邻两个活跃thread进行归约，</p><p>现在是二分后，等分的后半部分的thread，归约到前半段。</p><p><img src="/images/Pasted%20image%2020260522220853.png"></p><p>其实这里也优雅的解决了bank conflict的问题<br><img src="/images/Pasted%20image%2020260522222153.png"></p><p><img src="/images/Pasted%20image%2020260522222209.png"><br>因为他是同一个thread来访问同一个bank所以不叫bank conflict</p><p>我们说的bank conflict 是多个thread，访问同一个bank<br><img src="/images/Pasted%20image%2020260522222307.png"><br><img src="/images/Pasted%20image%2020260522224858.png"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">    <span class="comment">//归约</span></span><br><span class="line">    <span class="comment">//不断只剩一半的活跃线程</span></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> 0</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">1</span>; s&lt;blockDim.x;s*=<span class="number">2</span>)&#123;</span><br><span class="line">        <span class="keyword">if</span>(tid % (<span class="number">2</span>*s) == <span class="number">0</span>)&#123;</span><br><span class="line">            sdata[tid] += sdata[tid + s];</span><br><span class="line">        &#125;</span><br><span class="line">        __syncthreads();</span><br><span class="line">    &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line">   <span class="keyword">for</span>(<span class="type">int</span> s = blockDim.x / <span class="number">2</span>; s &gt;=<span class="number">1</span>; s &gt;&gt;=<span class="number">1</span>)&#123;</span><br><span class="line">        <span class="keyword">if</span>(tid &lt; s)&#123;</span><br><span class="line">            sdata[tid] += sdata[tid + s];</span><br><span class="line">        &#125;</span><br><span class="line">        __syncthreads();</span><br><span class="line">   &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><p>这里改动后，这一版的线程不再发散了，SM利用率和调度器发射率显著提升。</p><h3 id="优化3：展开最后一个warp"><a href="#优化3：展开最后一个warp" class="headerlink" title="优化3：展开最后一个warp"></a>优化3：展开最后一个warp</h3><p>就是说，当归约到就剩前32个thred之后，就已经处于一个warp里面了，这样就不需要再每次一半访问共享内存了，而是直接warp内归约，直接用寄存器</p><p><img src="/images/Pasted%20image%2020260522231144.png"></p><p>但是教程里面还有一点技巧</p><p><img src="/images/Pasted%20image%2020260522231159.png"></p><h3 id="另一种实现方式：BlockReduce"><a href="#另一种实现方式：BlockReduce" class="headerlink" title="另一种实现方式：BlockReduce"></a>另一种实现方式：BlockReduce</h3><p>用warp内归约，warpReduce的基础上，进一步构建blockReduce,来实现整个block内的归约操作。<br><img src="/images/Pasted%20image%2020260522231538.png"></p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">__inline__ __device__ <span class="type">float</span> <span class="title">block_reduce</span><span class="params">(<span class="type">float</span> val)</span> </span>&#123;</span><br><span class="line">  <span class="type">const</span> <span class="type">int</span> tid = threadIdx.x;</span><br><span class="line">  <span class="type">const</span> <span class="type">int</span> warpSize = <span class="number">32</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> offset = warpSize / <span class="number">2</span>; offset &gt; <span class="number">0</span>; offset /= <span class="number">2</span>)</span><br><span class="line">    val += __shfl_down_sync(<span class="number">0xFFFFFFFF</span>, val, offset);</span><br><span class="line"></span><br><span class="line">  __shared__ <span class="type">float</span> warpSum[<span class="number">32</span>];</span><br><span class="line">  <span class="keyword">if</span> (tid % warpSize == <span class="number">0</span>) &#123;</span><br><span class="line">     warpSum[tid / warpSize] = val;</span><br><span class="line">  &#125;</span><br><span class="line">  __syncthreads();</span><br></pre></td></tr></table></figure><p><code>#pragma unroll</code> 告诉 nvcc <strong>强制展开</strong>这个循环。</p><p>这个循环只有 5 次迭代（offset &#x3D; 16, 8, 4, 2, 1），展开后等价于：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">val += __shfl_down_sync(0xFFFFFFFF, val, 16);</span><br><span class="line">val += __shfl_down_sync(0xFFFFFFFF, val, 8);</span><br><span class="line">val += __shfl_down_sync(0xFFFFFFFF, val, 4);</span><br><span class="line">val += __shfl_down_sync(0xFFFFFFFF, val, 2);</span><br><span class="line">val += __shfl_down_sync(0xFFFFFFFF, val, 1);</span><br></pre></td></tr></table></figure><p>三个好处：</p><ol><li><strong>消除循环开销</strong> — 不用计数器、不用比较、不用跳转，5 条指令直接排开</li><li><strong>编译期优化</strong> — offset 变成编译期常量，nvcc 可以更好地做指令调度和寄存器分配</li><li><strong>确定性</strong> — 虽然 nvcc 对小循环通常会自动展开，但加 <code>#pragma unroll</code> 是强制保证，避免编译器”犯懒”</li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><img src="/images/Pasted%20image%2020260522231923.png"></p><h2 id="矩阵乘-Matmul-基础"><a href="#矩阵乘-Matmul-基础" class="headerlink" title="矩阵乘 Matmul 基础"></a>矩阵乘 Matmul 基础</h2><h3 id="基础实现"><a href="#基础实现" class="headerlink" title="基础实现"></a>基础实现</h3><p><img src="/images/8e99e75e5e531575a18eaf10d936ad65%201.jpg"><br>就是最直接的，一个thread负责计算一个输出矩阵的元素。</p><p>我们认为，当前的实现方式带来了3个显著的性能瓶颈：</p><ol><li><p><strong>全局内存访问延迟高</strong>：每个线程都要从全局内存中加载输入矩阵 A 和 B 的数据，访问延迟较高，严重限制了整体吞吐效率；</p></li><li><p><strong>计算密度低，硬件利用率差</strong>：每个线程仅执行一次规约计算（即一次点积中的部分乘加），却需要多次访问全局内存，造成计算&#x2F;访存比（compute-to-memory ratio）极低；</p><blockquote><p>两次浮点运算，需要两次访问全局内存，效率极低</p></blockquote></li><li><p><strong>缺乏对共享显存的有效利用</strong>：访问模式缺乏数据复用，GPU 的缓存机制无法发挥应有的加速作用。</p></li></ol><p><img src="/images/Pasted%20image%2020260525225818.png"></p><h3 id="优化：共享内存"><a href="#优化：共享内存" class="headerlink" title="优化：共享内存"></a>优化：共享内存</h3><p>核心思路就是：<strong>不要让每个 thread 每次都直接从 global memory 读 A 和 B，而是让一个 block 先把一小块 A 和一小块 B 搬到 shared memory，然后 block 内的 thread 反复复用这些数据</strong></p><p>本质上就是利用<mark style="background:#fff88f">分块矩阵的乘法</mark>，一个block来计算一个分块矩阵的乘法，</p><p><mark style="background:#fff88f">这样对于某一个 block 正在处理的这一轮 K 分块，A_tile 和 B_tile 从 global memory 加载一次到 shared memory，然后被 block 内很多线程反复复用。</mark></p><p>这样输出矩阵中<mark style="background:#fff88f">这个分块矩阵里面所有元素的计算都仅需要访问共享内存即可</mark></p><p><img src="/images/309c29197f495b899cfdbfbf47b83b45.jpg"></p><p>假设每个A的子矩阵是(32x1024)<br>所以M &#x3D; N &#x3D; K &#x3D;1024<br>bm &#x3D; 32<br>bn &#x3D; 32<br>bk &#x3D; 1024</p><p>但是这样有一个需要注意的点，如果需要一个block来计算C_tile (b_m x b_n)</p><p>那么所需要的共享内存为2 x (32 x 1024) 太大了。</p><p>所以这里每个block不是一次性拷贝A，B的分块。而是分批拷贝<br><img src="/images/9c95276756652b8c6be811863714a8b1.jpg"></p><p>所以定义的共享内存是 2 x （32 x 32）</p><p><img src="/images/29c2339d615058e1d6c4ddde4e16c63e.jpg"><br>所以，共享内存里面存放的是一个C_tile里面所有元素在每个Aip小块上的分量。也就是Aip</p><p>重新梳理一下就是：<br><img src="/images/d0be94d5c78c42f28f39c75c225c8dbe.jpg"><br><img src="/images/Pasted%20image%2020260525224153.png"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">针对v1每个thread都要访问全局内存：</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">v2，利用分块矩阵+block内共享内存，减少全局内存的访问次数</span></span><br><span class="line"><span class="comment">block阵列，但是block内thread分布是一维的thread，</span></span><br><span class="line"><span class="comment">            block内的thread们，按照C_tile的元素的行优先来分配</span></span><br><span class="line"><span class="comment">所以本质上，还是一个thread对应C的一个元素，只不过是分块block来管理</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SUB_SIZE 32<span class="comment">//这里假装A的分块，B的分块都是相同的尺寸</span></span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v2</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span> *A, </span></span></span><br><span class="line"><span class="params"><span class="function">                                                <span class="type">float</span>* B, <span class="type">float</span> beta, <span class="type">float</span> *C)</span></span>&#123;</span><br><span class="line">    <span class="comment">//自己写一遍</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//1. 定义该block相关信息：</span></span><br><span class="line">    <span class="type">int</span> blocks_x = blockIdx.x;</span><br><span class="line">    <span class="type">int</span> blocks_y = blockIdx.y;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//2. block对应分块矩阵</span></span><br><span class="line">    <span class="comment">//一个block内有固定个数的thread，数量 = C_tile.shape = (b_m, b_n)</span></span><br><span class="line">    <span class="comment">//一个block对应的A_sub = (b_m, K)</span></span><br><span class="line">    <span class="comment">//一个block对应的B_sub = (K, b_n)</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_m = SUB_SIZE;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_n = SUB_SIZE;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_k = SUB_SIZE;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//3. block对应C_tile内部索引, 行优先</span></span><br><span class="line">    <span class="type">int</span> ty = threadIdx.x / b_n;</span><br><span class="line">    <span class="type">int</span> tx = threadIdx.x % b_n;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//4. 定义共享内存，保存A_sub的切片(Aip), B_sub的切片(Bpj)</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[b_m * b_k];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[b_k * b_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//5. 定义每个切片的开始元素在内存里面的地址, 行优先</span></span><br><span class="line">    A = &amp;A[blocks_y*b_m*K];<span class="comment">//A指向A_sub的第一个元素</span></span><br><span class="line">    B = &amp;B[blocks_x*b_n];<span class="comment">//B指向B_sub的第一个元素</span></span><br><span class="line">    C = &amp;C[blocks_y*b_m*N+blocks_x*b_n];<span class="comment">//C指向了C_tile的第一个元素</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//5. 开始计算该thread对应的C_tile(ty, tx)元素的计算了。</span></span><br><span class="line">    <span class="comment">//对于当前的(ty, tx)元素, ty有b_m种， tx有b_n种 = b_k种</span></span><br><span class="line">    <span class="type">float</span> tmp = <span class="number">0.0f</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> k = <span class="number">0</span>; k&lt;K; k+=b_k)&#123;</span><br><span class="line">        <span class="comment">//先挨个把A_tile, B_tile拷贝到shared memory</span></span><br><span class="line">        As[ty*b_k + tx] = A[ty*K + tx];</span><br><span class="line">        Bs[ty*b_n + tx] = B[ty*N + tx];</span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//指向下一个切片A_tile, B_tile</span></span><br><span class="line">        A += b_k;</span><br><span class="line">        B += N*b_k;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//计算C_tile(ty, tx)在该切片上的分量（A_tile行 x B_tile列）</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> i =<span class="number">0</span>; i&lt;b_k; i++)&#123;</span><br><span class="line">            tmp += As[ty*b_k + i] * Bs[i*b_n + tx];</span><br><span class="line">        &#125;</span><br><span class="line">        __syncthreads();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//tmp为每个元素的值</span></span><br><span class="line">    C[tx + ty*N] = alpha*tmp + beta*C[tx + ty*N];</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260525225801.png"></p><p>这样可以<strong>减少拷贝重复</strong>，<mark style="background:#fff88f">v2里面，每个thread仅仅拷贝对应位置的float</mark>，<mark style="background:#fff88f">所以整个全局内存的每个float仅仅被访问过一次</mark>。</p><p>v1里面，相邻两个C元素的计算，访问的全局内存，有一半都是重复的。</p><p><img src="/images/Pasted%20image%2020260525230533.png" alt="285"><br><img src="/images/Pasted%20image%2020260525230546.png"><br><img src="/images/Pasted%20image%2020260525230646.png"></p><p><strong>分析软件也给出了优化建议</strong></p><p><img src="/images/Pasted%20image%2020260525230904.png"></p><h3 id="优化：Thread-Tile"><a href="#优化：Thread-Tile" class="headerlink" title="优化：Thread Tile"></a>优化：Thread Tile</h3><p>上面的两种优化实现思路，我们依然是保持，<mark style="background:#fff88f">每个thread仅计算C中的一个元素</mark>。这会导致<strong>计算访存比</strong>（Compute-to-Memory Ratio）并未得到明显改善，<strong>因为每个计算迭代依然需要两次访存操作</strong>（加载 A 和 B 的数据）和一次计算操作，访存瓶颈依然存在</p><blockquote><p>大白话就是说：现在每个thread计算C的元素，依然要从全局内存中访问对应的A的一行，B的一列，只不过，把他们打包然后一起拷贝到共享内存中一起算。但是<strong>每个thread都需要访存</strong>。</p></blockquote><p>现在引入了 <strong>thread tile（线程瓦片）</strong> 技术</p><p>不再让一个线程处理一个数据元素，而是让每个线程负责处理一个大小为 TM×TN 的数据 <strong>tile</strong></p><p><img src="/images/bf9a61bb751d8f82ced61ada7b10bea6.jpg"></p><p><img src="/images/28f2678cbbeb1e029cf2b6b2537b52f1.jpg"><br><img src="/images/f576d2e5676490448b0043ede7a4cdc5.jpg"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">V3, 基于V2优化，block内一个thread负责多个输出元素的计算</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m, <span class="type">const</span> <span class="type">int</span> b_k, <span class="type">const</span> <span class="type">int</span> b_n, <span class="type">const</span> <span class="type">int</span> t_m, <span class="type">const</span> <span class="type">int</span> t_n&gt;</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v3</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span> *A, <span class="type">float</span>* B, <span class="type">float</span> beta, <span class="type">float</span> *C)</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//1. 首先得到block阵列编号对应的sub编号</span></span><br><span class="line">    <span class="type">int</span> blocks_y = blockIdx.y;</span><br><span class="line">    <span class="type">int</span> blocks_x = blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//1.1 定义总共多少个thread数量</span></span><br><span class="line">    <span class="type">int</span> thread_num = (b_n / t_n) * (b_m / t_m);</span><br><span class="line">    <span class="keyword">if</span>(threadIdx.x &gt;= thread_num)&#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//2. A,B指向A_sub, B_sub</span></span><br><span class="line">    A = &amp;A[blocks_y *b_m* K];</span><br><span class="line">    B = &amp;B[blocks_x*b_n];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 3. 给block内的thread分配block内索引索引</span></span><br><span class="line">    <span class="type">int</span> ty = threadIdx.x / (b_n / t_n);</span><br><span class="line">    <span class="type">int</span> tx = threadIdx.x % (b_n / t_n);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 2.1, C指向C_tile </span></span><br><span class="line">    C = &amp;C[blocks_y*b_m*N + blocks_x*b_n + ty*t_m*N + tx*t_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//4. 定义切片A_tile, B_tile的长度为b_k,为他们分配共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[b_m*b_k];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[b_k*b_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 5. 拷贝任务分配给各个thread的分配细节。</span></span><br><span class="line">    <span class="comment">// As: b_m*b_k, 行分满。指定该thread拷贝的该A_tile中的float的位置</span></span><br><span class="line">    <span class="type">int</span> a_copy_y = threadIdx.x / b_k;</span><br><span class="line">    <span class="type">int</span> a_copy_x = threadIdx.x % b_k;</span><br><span class="line">    <span class="type">int</span> a_copy_stride = thread_num/b_k; <span class="comment">//所有thread一起拷贝，最多拷贝这么多行</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> b_copy_y = threadIdx.x / b_n;</span><br><span class="line">    <span class="type">int</span> b_copy_x = threadIdx.x % b_n;</span><br><span class="line">    <span class="type">int</span> b_copy_stride = thread_num / b_n;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 6. 开始拷贝每个切片</span></span><br><span class="line">    <span class="type">float</span> tmp[t_m][t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    <span class="type">float</span>* M_p = &amp;As[ty*t_m*b_k];</span><br><span class="line">    <span class="type">float</span>* N_p = &amp;Bs[tx*t_n];</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i&lt;K; i+=b_k)&#123;</span><br><span class="line">        <span class="comment">//拷贝a_tile</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">0</span>; s&lt;b_m;s+=a_copy_stride)&#123;</span><br><span class="line">            As[(a_copy_y + s)*b_k + a_copy_x]=A[(a_copy_y + s)*K + a_copy_x];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//拷贝b_tile</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">            Bs[(b_copy_y + s)*b_n + b_copy_x] = B[(b_copy_y + s)*N + b_copy_x];</span><br><span class="line">        &#125;</span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//A，B位移，指向下一个tile</span></span><br><span class="line">        A += b_k;</span><br><span class="line">        B += b_k*N;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//计算Mp x Np (t_m x t_n)</span></span><br><span class="line">        <span class="comment">/* //第一种方法：先定外层的t_m, t_n, 再循环内层的l in b_k</span></span><br><span class="line"><span class="comment">        for(int m =0;m &lt; t_m; m++)&#123;</span></span><br><span class="line"><span class="comment">            for(int n=0; n&lt; t_n; n++)&#123;</span></span><br><span class="line"><span class="comment">                //计算向量乘</span></span><br><span class="line"><span class="comment">                float v = 0.f;</span></span><br><span class="line"><span class="comment">                for(int l=0;l&lt;b_k;l++)&#123;</span></span><br><span class="line"><span class="comment">                    v+= M_p[m*b_k + l] * N_p[n +l*b_n];</span></span><br><span class="line"><span class="comment">                &#125;</span></span><br><span class="line"><span class="comment">                tmp[m][n] += v;</span></span><br><span class="line"><span class="comment">            &#125;</span></span><br><span class="line"><span class="comment">        &#125;</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//第二种方法：先定内层的循环l, 再定外层的m,n 指定计算的元素</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> l =<span class="number">0</span>; l &lt; b_k; l++)&#123;</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt; t_n; n++)&#123;</span><br><span class="line">                    tmp[m][n] += M_p[m*b_k + l] * N_p[n + l*b_n];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        __syncthreads();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i&lt;t_m;i++)&#123;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> j =<span class="number">0</span>; j&lt;t_n;j++)&#123;</span><br><span class="line">            C[i*N+j] = alpha*tmp[i][j] + beta*C[i*N + j];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="优化：向量化加载-转置hit"><a href="#优化：向量化加载-转置hit" class="headerlink" title="优化：向量化加载+转置hit"></a>优化：向量化加载+转置hit</h3><p><img src="/images/Pasted%20image%2020260526175742.png"></p><blockquote><p>可以看到，前面三个版本，对于每个输出元素，都需要挨个取A向量，B向量的元素，然后乘加。</p></blockquote><p>使用<strong>向量访存指令</strong>，一次性读取同一行中的多个元素</p><p>一个线程不仅要读取TM×TN数量的数据，还要用<strong>更少的指令进行加载</strong></p><p>V4 中，我们的优化重点从计算并行转向了<strong>访存向量化</strong></p><p><img src="/images/Pasted%20image%2020260526180213.png"><br><img src="/images/Pasted%20image%2020260526180235.png" alt="402"></p><p>因为是优化的读取全局内存，所以是<strong>优化的从A_tile，拷贝到共享内存的这一个部分</strong></p><p>所以，<mark style="background:#fff88f">第一个优化点：拷贝全局内存到共享内存阶段：一次读多个float</mark></p><p><img src="/images/Pasted%20image%2020260526191906.png"><br>这里，当m增加的时候，就是计算每一行的时候，M_p访问都是访问一列，对于M_p这个共享内存的计算效率不高<br><img src="/images/d9f6a5e024e7d9a289289e44f43ed38f.jpg" alt="265"></p><blockquote><p>也就是说，你在拷贝阶段拷贝出来的As，我在用的阶段，是访问一列的。而Bs，在用的时候，主要是n在变，所以是访问一行<br>As用的时候，主要是m在变，访问一列，所以这里也要优化。</p></blockquote><p><mark style="background:#fff88f">所以第二个优化点：在从共享内存里面读取数据来使用：As更频繁的访问是访问一列数据，所以这里在前一个阶段，可以直接存转置的</mark></p><p><img src="/images/Pasted%20image%2020260526192940.png"></p><p><img src="/images/6fb9a5fc28bac9d762e0cecd74eed952.jpg"><br><img src="/images/18856483705dc7764f19beda243449bb.jpg"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">V4: 用向量预取指令，优化V3（从a_tile拷贝到As）</span></span><br><span class="line"><span class="comment">    优化点：</span></span><br><span class="line"><span class="comment">        1. 全局内存A_tile拷贝到共享内存As，一个thread一次读4个float</span></span><br><span class="line"><span class="comment">        2. 写共享内存As直接写转置，对应读共享内存时，优化读取的效率</span></span><br><span class="line"><span class="comment">        3. 计算结果从寄存器写回全局内存C_tile, 同样一次写4个float</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="comment">//定义超参数</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m, <span class="type">const</span> <span class="type">int</span> b_k, <span class="type">const</span> <span class="type">int</span> b_n, <span class="type">const</span> <span class="type">int</span> t_m, <span class="type">const</span> <span class="type">int</span> t_n&gt;</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v4</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span> beta, <span class="type">float</span> *A, <span class="type">float</span> *B, <span class="type">float</span>*C)</span></span>&#123;</span><br><span class="line">    <span class="comment">//1. 找出对应的blocks的索引，为了后面找出A_sub, B_sub, C_sub</span></span><br><span class="line">    <span class="type">int</span> blocks_y = blockIdx.y;</span><br><span class="line">    <span class="type">int</span> blocks_x = blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//2. A，B指向各自的sub子块</span></span><br><span class="line">    A = &amp;A[blocks_y*b_m*K];</span><br><span class="line">    B = &amp;B[blocks_x*b_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 3. 定义block中线程的量：</span></span><br><span class="line">    <span class="type">int</span> thread_sum = (b_n/t_n)*(b_m/t_m);</span><br><span class="line">    <span class="keyword">if</span>(threadIdx.x &gt;= thread_sum)</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ty = threadIdx.x / (b_n/t_n);<span class="comment">//(b_n/t_n)=C_sub一行需要多少个thread</span></span><br><span class="line">    <span class="type">int</span> tx = threadIdx.x % (b_n/t_n);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 3. C指向C_sub中该thread的c_tile</span></span><br><span class="line">    C = &amp;C[blocks_y*b_m*N + blocks_x*b_n + ty*t_m*N + tx*t_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//优化点2: As存储用转置</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[b_k * b_m];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[b_k * b_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//$$ 4. 该thread在A_tile中拷贝的索引,现在一个thread拷贝4个float</span></span><br><span class="line">    <span class="type">int</span> a_copy_y = threadIdx.x / (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> a_copy_x = threadIdx.x % (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> a_copy_stride = thread_sum / (b_k / <span class="number">4</span>); <span class="comment">//所有thread一次最多完整拷贝a_copy_stride行</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> b_copy_y = threadIdx.x / (b_n / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> b_copy_x = threadIdx.x % (b_n / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> b_copy_stride = thread_sum / (b_n / <span class="number">4</span>);<span class="comment">//所有thread一次最多完整拷贝b_copy_stride行</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//开始处理每个A_tile, B_tile</span></span><br><span class="line">    <span class="type">float</span> tmp[t_m][t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> k =<span class="number">0</span>; k &lt; K; k+=b_k)&#123;</span><br><span class="line">        <span class="comment">//全局内存拷贝到共享内存</span></span><br><span class="line">        <span class="comment">//A_tile -&gt; As(转置)</span></span><br><span class="line">        float4 src = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">        <span class="type">float</span>* dst = <span class="literal">nullptr</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s &lt; b_m; s+=a_copy_stride)&#123;</span><br><span class="line">            <span class="comment">//优化1：一次性从全局内存拷贝float4到寄存器</span></span><br><span class="line">            <span class="comment">//先偏移到每个stride步的开头</span></span><br><span class="line">            src = *(float4*)&amp;A[s*K + a_copy_y*K + a_copy_x*<span class="number">4</span>];</span><br><span class="line">            <span class="comment">//转置写入As</span></span><br><span class="line">            <span class="comment">// As[s*b_k + a_copy_y*b_k + a_copy_x * 4] = src.x;</span></span><br><span class="line">            <span class="comment">// As[s*b_k + a_copy_y*b_k + a_copy_x * 4 + b_m] = src.y;</span></span><br><span class="line">            <span class="comment">// As[s*b_k + a_copy_y*b_k + a_copy_x * 4 + b_m*2] = src.z;</span></span><br><span class="line">            <span class="comment">// As[s*b_k + a_copy_y*b_k + a_copy_x * 4 + b_m*3] = src.w;</span></span><br><span class="line">            As[a_copy_x*<span class="number">4</span>*b_m + s + a_copy_y] = src.x;</span><br><span class="line">            As[a_copy_x*<span class="number">4</span>*b_m + s + a_copy_y + b_m] = src.y;</span><br><span class="line">            As[a_copy_x*<span class="number">4</span>*b_m + s + a_copy_y + b_m*<span class="number">2</span>] = src.z;</span><br><span class="line">            As[a_copy_x*<span class="number">4</span>*b_m + s + a_copy_y + b_m*<span class="number">3</span>] = src.w;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment">//B_tile -&gt; Bs</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_k; s += b_copy_stride)&#123;</span><br><span class="line">            *(float4*)&amp;Bs[s*b_n + b_copy_y*b_n + b_copy_x*<span class="number">4</span>] = *(float4*)&amp;B[s*N + b_copy_y*N + b_copy_x*<span class="number">4</span>];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//指向下一个p切片A_tile, B_tile</span></span><br><span class="line">        A += b_k;</span><br><span class="line">        B += b_k*N;</span><br><span class="line"></span><br><span class="line">        </span><br><span class="line"></span><br><span class="line">        <span class="comment">//开始计算Mp x Np, 结果保存在tmp中</span></span><br><span class="line">        <span class="comment">//l是向量乘的第几项</span></span><br><span class="line">        <span class="type">float</span>* M_p = &amp;As[ty*t_m];</span><br><span class="line">        <span class="type">float</span>* N_p = &amp;Bs[tx*t_n];</span><br><span class="line">        <span class="type">float</span> a_tile_col_l[t_m];</span><br><span class="line">        <span class="type">float</span> b_tile_row_l[t_n];</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> l = <span class="number">0</span>; l&lt;b_k; l++)&#123;</span><br><span class="line">            <span class="comment">//共享内存一口气拷贝a_tile的l列中所有的行 到寄存器</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m;m+=<span class="number">4</span>)&#123;</span><br><span class="line">                *(float4*)&amp;a_tile_col_l[m] = *(float4*)&amp;M_p[l*b_m + m];</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">//共享内存一口气拷贝b_tile的l行中所有的列 到寄存器</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">                *(float4*)&amp;b_tile_row_l[n] = *(float4*)&amp;N_p[l*b_n + n];</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n &lt; t_n; n++)&#123;</span><br><span class="line">                    tmp[m][n] += a_tile_col_l[m] * b_tile_row_l[n];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line">    <span class="comment">//此时tmp已经全部完整计算，c_tile所有元素的l个分量全部完整。</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m++)&#123;</span><br><span class="line">        <span class="comment">//每一行C</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">            float4 C_temp = *(float4*)&amp;C[m*N + n];</span><br><span class="line">            C_temp.x = alpha*tmp[m][n] + beta*C_temp.x;</span><br><span class="line">            C_temp.y = alpha*tmp[m][n<span class="number">+1</span>] + beta*C_temp.y;</span><br><span class="line">            C_temp.z = alpha*tmp[m][n<span class="number">+2</span>] + beta*C_temp.z;</span><br><span class="line">            C_temp.w = alpha*tmp[m][n<span class="number">+3</span>] + beta*C_temp.w;</span><br><span class="line">            *(float4*)&amp;C[m*N + n] = C_temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="优化：双缓冲队列"><a href="#优化：双缓冲队列" class="headerlink" title="优化：双缓冲队列"></a>优化：双缓冲队列</h3><p>在V4的优化版本中，我们主要是在</p><ul><li>全局-&gt;共享</li><li>共享-&gt;寄存器<br>这几个访存阶段，通过float4，来提升访存效率（一次访问，获取多个数据。）</li></ul><p>但是整体的计算，都串行在访存的指令后面，必须严格等待访存结束后，__syncthreads()，才能使用这些数据进行计算。</p><p><img src="/images/3ac04ccafec5173c776b4078bef2462b%201.jpg"></p><p>这导致<mark style="background:#ff4d4f">访存与计算完全串行</mark>，GPU 的计算单元在<strong>等待数据时大量空闲</strong>，造成算力浪费。为解决这一问题，我们将引入一种经典且高效的优化技术：<strong>双缓冲（Double Buffering）</strong></p><p><strong>核心优化思想</strong>是将访存与计算阶段进行重叠——当一组数据正在被计算时，另一组数据可以提前从显存中异步加载到缓冲区</p><p><mark style="background:#fff88f">所以，这里的双缓冲区，也涉及两个地方</mark>：</p><ul><li>全局-&gt;共享内存<ul><li>读全局的下一个时刻数据到共享内存2，下面计算用共享内存1中当前时刻的数据。</li><li><img src="/images/Pasted%20image%2020260527144142.png" alt="387"></li></ul></li><li>共享内存-&gt;寄存器<ul><li>读共享内存中下一时刻的数据到寄存器2，下面计算用寄存器1中当前时刻的数据。</li><li><img src="/images/Pasted%20image%2020260527144208.png"></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260527144113.png"></p><p><img src="/images/Pasted%20image%2020260527144313.png"></p><blockquote><p>这是根据自己的逻辑的理解，写的一版，但是只是框架对了，并没有并行，所以效果很烂</p></blockquote><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">V5: 用双缓冲区，让编译器优化指令流水线中数据拷贝和计算的部分进行重叠。</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OFFSET(row, col, row_dim) ((row)*(row_dim) + (col))</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//float pointer</span></span><br><span class="line"><span class="comment">//等价于*(float4*)&amp;pointer[], 就是按float4*类型来解析地址，访问float4内存</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> FETCH_FLOAT4(pointer) (reinterpret_cast<span class="string">&lt;float4*&gt;</span>(&amp;(pointer))[0])</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m, <span class="type">const</span> <span class="type">int</span> b_k, <span class="type">const</span> <span class="type">int</span> b_n, <span class="type">const</span> <span class="type">int</span> t_m, <span class="type">const</span> <span class="type">int</span> t_n&gt;</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v5</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span> *A, <span class="type">float</span>*B, <span class="type">float</span> beta, <span class="type">float</span>* C)</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//1. 计算该block的索引，知道自己处理那一个sub</span></span><br><span class="line">    <span class="type">int</span> blocks_x = blockIdx.x;</span><br><span class="line">    <span class="type">int</span> blocks_y = blockIdx.y;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//2. 重定向A，B， 指向A_sub, B_sub</span></span><br><span class="line">    A = &amp;A[<span class="built_in">OFFSET</span>(blocks_y*b_m, <span class="number">0</span>,K)];</span><br><span class="line">    B = &amp;B[<span class="built_in">OFFSET</span>(blocks_x*b_n, <span class="number">0</span>, <span class="number">1</span>)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//3. 计算该thread的block内索引：</span></span><br><span class="line">    <span class="type">int</span> thread_num = (b_n/t_n) * (b_m/t_m);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//直接 blockDim.x = thread_num最好，不然后面__syncthreads()会被阻塞</span></span><br><span class="line">    <span class="comment">// if(threadIdx.x &gt;= thread_num)</span></span><br><span class="line">    <span class="comment">//     return;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ty = threadIdx.x / (b_n/t_n); <span class="comment">//一个C_sub的一行需要(b_n/t_n)个C_tile</span></span><br><span class="line">    <span class="type">int</span> tx = threadIdx.x % (b_n/t_n);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//4. 重定向C 到该thread的 C_tile</span></span><br><span class="line">    C = &amp;C[<span class="built_in">OFFSET</span>(blocks_y*b_m + ty * t_m, blocks_x*b_n + tx*t_n, N)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//5. 设置双缓冲区共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[<span class="number">2</span>][b_k*b_m];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[<span class="number">2</span>][b_k*b_n];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//6. 计算该thread负责拷贝A_tile的索引, 每个thread拷贝s*4个float</span></span><br><span class="line">    <span class="type">int</span> a_copy_y = threadIdx.x / (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> a_copy_x = threadIdx.x % (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> a_copy_stride = thread_num / (b_k / <span class="number">4</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//7. 计算该thread负责拷贝B_tile的索引，每个thread拷贝s*4个float</span></span><br><span class="line">    <span class="type">int</span> b_copy_y = threadIdx.x / (b_n/<span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> b_copy_x = threadIdx.x % (b_n/<span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> b_copy_stride = thread_num / (b_n/<span class="number">4</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> buffer_index = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//8. 预先拷贝第一个切片（全局内存-&gt;共享内存）</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_m; s+=a_copy_stride)&#123;</span><br><span class="line">        <span class="comment">//获取该thread需要拷贝的float4</span></span><br><span class="line">        float4 src = <span class="built_in">FETCH_FLOAT4</span>(A[<span class="built_in">OFFSET</span>(s+a_copy_y, <span class="number">4</span>*a_copy_x,K)]);</span><br><span class="line">        As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span>, a_copy_y + s,b_m)] = src.x;</span><br><span class="line">        As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">1</span>, a_copy_y + s,b_m)] = src.y;</span><br><span class="line">        As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">2</span>, a_copy_y + s,b_m)] = src.z;</span><br><span class="line">        As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">3</span>, a_copy_y + s,b_m)] = src.w;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">        <span class="comment">//直接拷贝float4赋值</span></span><br><span class="line">        <span class="built_in">FETCH_FLOAT4</span>(Bs[buffer_index][<span class="built_in">OFFSET</span>(s+b_copy_y, b_copy_x*<span class="number">4</span>,b_n)])</span><br><span class="line">            = <span class="built_in">FETCH_FLOAT4</span>(B[<span class="built_in">OFFSET</span>(s+b_copy_y,b_copy_x*<span class="number">4</span>,N)]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    A += b_k;</span><br><span class="line">    B += <span class="built_in">OFFSET</span>(b_k, <span class="number">0</span>, N);</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//下面开始每个切片A_tile, B_tile, 计算C_tile在各个切片上的分量</span></span><br><span class="line">    <span class="type">float</span> tmp[t_m][t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> k = <span class="number">0</span>; k&lt;K; k+=b_k)&#123;</span><br><span class="line">        <span class="comment">//更新缓冲区的index</span></span><br><span class="line">        buffer_index = <span class="number">1</span> - buffer_index;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//全局内存-&gt;共享内存</span></span><br><span class="line">        <span class="comment">//8. 预先拷贝第一个切片（全局内存-&gt;共享内存）</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//排除最后一次的提前拷贝,比计算少一次</span></span><br><span class="line">        <span class="keyword">if</span>(k &lt; K-b_k)&#123;</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_m; s+=a_copy_stride)&#123;</span><br><span class="line">                <span class="comment">//获取该thread需要拷贝的float4</span></span><br><span class="line">                float4 src = <span class="built_in">FETCH_FLOAT4</span>(A[<span class="built_in">OFFSET</span>(s+a_copy_y, <span class="number">4</span>*a_copy_x,K)]);</span><br><span class="line">                As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span>, a_copy_y + s,b_m)] = src.x;</span><br><span class="line">                As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">1</span>, a_copy_y + s,b_m)] = src.y;</span><br><span class="line">                As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">2</span>, a_copy_y + s,b_m)] = src.z;</span><br><span class="line">                As[buffer_index][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">3</span>, a_copy_y + s,b_m)] = src.w;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">                <span class="comment">//直接拷贝float4赋值</span></span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(Bs[buffer_index][<span class="built_in">OFFSET</span>(s+b_copy_y, b_copy_x*<span class="number">4</span>,b_n)])</span><br><span class="line">                    = <span class="built_in">FETCH_FLOAT4</span>(B[<span class="built_in">OFFSET</span>(s+b_copy_y,b_copy_x*<span class="number">4</span>,N)]);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            A += b_k;</span><br><span class="line">            B += <span class="built_in">OFFSET</span>(b_k, <span class="number">0</span>, N);</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment">//计算区域，直接用1-buffer_index的共享内存缓冲区</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//定义寄存器双缓冲区</span></span><br><span class="line">        <span class="type">int</span> reg_buffer_index = <span class="number">0</span>;</span><br><span class="line">        <span class="type">float</span> a_tile_reg[<span class="number">2</span>][t_m] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">        <span class="type">float</span> b_tile_reg[<span class="number">2</span>][t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment">//先拷贝l=0时的寄存器</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m&lt;t_m; m+=<span class="number">4</span>)&#123;</span><br><span class="line">            <span class="built_in">FETCH_FLOAT4</span>(a_tile_reg[reg_buffer_index][m]) = <span class="built_in">FETCH_FLOAT4</span>(As[<span class="number">1</span>-buffer_index][<span class="built_in">OFFSET</span>(<span class="number">0</span>,ty*t_m+m,b_m)]);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">            <span class="built_in">FETCH_FLOAT4</span>(b_tile_reg[reg_buffer_index][n]) = <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="number">1</span>-buffer_index][<span class="built_in">OFFSET</span>(<span class="number">0</span>,tx*t_n + n,b_n)]);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">       </span><br><span class="line"></span><br><span class="line">        <span class="comment">//因为上面l=0已经拷贝过来，下面需要用到l，所以l只能从1开始计数，上面k仅用来计数，所以从0开始也无所谓。</span></span><br><span class="line">        <span class="comment">//l：1 ~ b_k-1</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> l = <span class="number">1</span>; l &lt; b_k; l++)</span><br><span class="line">        &#123;</span><br><span class="line">            reg_buffer_index = <span class="number">1</span>- reg_buffer_index;</span><br><span class="line">            <span class="comment">//对于每个分量</span></span><br><span class="line">            <span class="comment">//共享内存-&gt;寄存器 (下一时刻)</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">//因为已经拷贝过一次了，所以少一次拷贝</span></span><br><span class="line">            </span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(a_tile_reg[reg_buffer_index][m]) = <span class="built_in">FETCH_FLOAT4</span>(As[<span class="number">1</span>-buffer_index][<span class="built_in">OFFSET</span>(l,ty*t_m+m,b_m)]);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(b_tile_reg[reg_buffer_index][n]) = <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="number">1</span>-buffer_index][<span class="built_in">OFFSET</span>(l,tx*t_n + n,b_n)]);</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line"></span><br><span class="line">            <span class="comment">//计算</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n &lt; t_n; n++)&#123;</span><br><span class="line">                    tmp[m][n] += a_tile_reg[<span class="number">1</span>-reg_buffer_index][m] * b_tile_reg[<span class="number">1</span>-reg_buffer_index][n];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//计算</span></span><br><span class="line">        reg_buffer_index = <span class="number">1</span>- reg_buffer_index;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n &lt; t_n; n++)&#123;</span><br><span class="line">                tmp[m][n] += a_tile_reg[<span class="number">1</span>-reg_buffer_index][m] * b_tile_reg[<span class="number">1</span>-reg_buffer_index][n];</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">    <span class="comment">//对于C_tile，tmp的每一行</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m++)&#123;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> n=<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">            float4 C_temp = <span class="built_in">FETCH_FLOAT4</span>(C[<span class="built_in">OFFSET</span>(m,n,N)]);</span><br><span class="line">            C_temp.x = alpha*tmp[m][n] + beta*C_temp.x;</span><br><span class="line">            C_temp.y = alpha*tmp[m][n<span class="number">+1</span>] + beta*C_temp.y;</span><br><span class="line">            C_temp.z = alpha*tmp[m][n<span class="number">+2</span>] + beta*C_temp.z;</span><br><span class="line">            C_temp.w = alpha*tmp[m][n<span class="number">+3</span>] + beta*C_temp.w;</span><br><span class="line">            <span class="built_in">FETCH_FLOAT4</span>(C[<span class="built_in">OFFSET</span>(m,n,N)]) = C_temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>但是实际测试发现，这个效果并不好：所以发现，<mark style="background:#ff4d4f">这个写法其实是错误的。</mark><br><img src="/images/Pasted%20image%2020260527171357.png" alt="313"><br><img src="/images/Pasted%20image%2020260527171030.png"></p><p>所以，真正能够使用流水线指令重叠的流程应该是这样子的：</p><p><img src="/images/2fc463cedf397e438cf7fef85148ae14.jpg" alt="421"></p><p>根据上图，可以看到，</p><ul><li>除了一开始，先完成<ul><li>一个tile的从全局内存-&gt;共享内存</li><li>共享内存-&gt;寄存器缓冲区<ul><li>因为中间涉及写共享内存&#x2F;读共享内存，所以<mark style="background:#ff4d4f">必须要sync</mark></li></ul></li></ul></li><li>循环后面【BK, 2BK, …, K】(剩余的K个tile要处理，因为一开始的tile也没有处理)<ul><li><strong>启动下一个tile的全局内存-&gt;寄存器</strong>【BK，…, K-BK】(K-1个要拷贝)<ul><li><mark style="background:#fff88f">这里就是指令并行，因为下面的指令执行并不依赖上面，所以FMA指令直接发射</mark></li></ul></li><li><strong>共享内存-&gt;寄存器</strong>，（l : 【0：BK-2】）</li><li><strong>寄存器-&gt;计算</strong>（l : 【0：BK-2】）</li><li>启动下一个tile的寄存器-&gt;共享内存【BK，…, K-BK】(K-1个要拷贝)</li><li><mark style="background:#ff4d4f">sync，等下一个tile的共享内存写完</mark></li><li>下一个tile共享内存-&gt;寄存器缓冲区（l&#x3D;0）</li><li>最后完成本次最后一个寄存器-&gt;计算（l &#x3D; BK-1）</li></ul></li></ul><p>关于为什么能够指令重叠的详细解释：</p><p><img src="/images/Pasted%20image%2020260527232433.png" alt="458"></p><p>但是要注意，<mark style="background:#ff4d4f">刚写了指令重叠的逻辑，还不够，要需要unroll编译展开</mark></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br></pre></td></tr></table></figure><p>来拆开循环，这样指令才能进行重叠</p><p><img src="/images/Pasted%20image%2020260528111232.png"></p><p>所以，最终实现如下</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * V5.2 修正指令重叠</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m, <span class="type">const</span> <span class="type">int</span> b_k, <span class="type">const</span> <span class="type">int</span> b_n, <span class="type">const</span> <span class="type">int</span> t_m, <span class="type">const</span> <span class="type">int</span> t_n&gt;</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v5_2</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span> *A, <span class="type">float</span>*B, <span class="type">float</span> beta, <span class="type">float</span>* C)</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//1. 计算该block的索引，知道自己处理那一个sub</span></span><br><span class="line">    <span class="type">int</span> blocks_x = blockIdx.x;</span><br><span class="line">    <span class="type">int</span> blocks_y = blockIdx.y;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//2. 重定向A，B， 指向A_sub, B_sub</span></span><br><span class="line">    A = &amp;A[<span class="built_in">OFFSET</span>(blocks_y*b_m, <span class="number">0</span>,K)];</span><br><span class="line">    B = &amp;B[<span class="built_in">OFFSET</span>(blocks_x*b_n, <span class="number">0</span>, <span class="number">1</span>)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//3. 计算该thread的block内索引：</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> thread_num = (b_n/t_n) * (b_m/t_m);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//直接 blockDim.x = thread_num最好，不然后面__syncthreads()会被阻塞</span></span><br><span class="line">    <span class="comment">// if(threadIdx.x &gt;= thread_num)</span></span><br><span class="line">    <span class="comment">//     return;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> ty = threadIdx.x / (b_n/t_n); <span class="comment">//一个C_sub的一行需要(b_n/t_n)个C_tile</span></span><br><span class="line">    <span class="type">int</span> tx = threadIdx.x % (b_n/t_n);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//4. 重定向C 到该thread的 C_tile</span></span><br><span class="line">    C = &amp;C[<span class="built_in">OFFSET</span>(blocks_y*b_m + ty * t_m, blocks_x*b_n + tx*t_n, N)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//5. 设置双缓冲区共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[<span class="number">2</span>][b_k*b_m];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[<span class="number">2</span>][b_k*b_n];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//6. 计算该thread负责拷贝A_tile的索引, 每个thread拷贝s*4个float</span></span><br><span class="line">    <span class="type">int</span> a_copy_y = threadIdx.x / (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> a_copy_x = threadIdx.x % (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> a_copy_stride = thread_num / (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> a_stride_num = b_m / a_copy_stride;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//7. 计算该thread负责拷贝B_tile的索引，每个thread拷贝s*4个float</span></span><br><span class="line">    <span class="type">int</span> b_copy_y = threadIdx.x / (b_n/<span class="number">4</span>);</span><br><span class="line">    <span class="type">int</span> b_copy_x = threadIdx.x % (b_n/<span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_copy_stride = thread_num / (b_n/<span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_stride_num = b_k / b_copy_stride;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//定义当前要从全局内存写入共享内存缓冲区的index</span></span><br><span class="line">    <span class="type">int</span> shared_index = <span class="number">0</span>;<span class="comment">//写入index</span></span><br><span class="line">    <span class="comment">//定义当前要从共享内存写入寄存器缓冲区的index</span></span><br><span class="line">    <span class="type">int</span> register_index = <span class="number">0</span>;<span class="comment">//写入index</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//对于当前l, Mp的所有m行，Np的所有n列</span></span><br><span class="line">    <span class="type">float</span> a_reg_l[<span class="number">2</span>][t_m] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    <span class="type">float</span> b_reg_l[<span class="number">2</span>][t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//【全局内存-&gt;共享内存环节】每个thread在所有stride步需要拷贝的float4</span></span><br><span class="line">    float4 A_tile_src[a_stride_num] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    float4 B_tile_src[b_stride_num] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//存储C_tile的每个元素</span></span><br><span class="line">    <span class="type">float</span> tmp[t_m][t_n] = &#123;<span class="number">0.0f</span>&#125;;</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 class="comment">//【先拷贝第一个tile】【全局内存-&gt;寄存器-&gt;共享内存】</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_m;s+=a_copy_stride)&#123;</span><br><span class="line">        <span class="comment">//1. 全局内存-&gt;寄存器</span></span><br><span class="line">        A_tile_src[s/a_copy_stride] = <span class="built_in">FETCH_FLOAT4</span>(A[<span class="built_in">OFFSET</span>(s+a_copy_y, <span class="number">4</span>*a_copy_x, K)]);</span><br><span class="line">        <span class="comment">//2. 寄存器-&gt;共享内存 (As是转置)</span></span><br><span class="line">        As[<span class="number">0</span>][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span>,s + a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].x;</span><br><span class="line">        As[<span class="number">0</span>][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">1</span>,s + a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].y;</span><br><span class="line">        As[<span class="number">0</span>][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">2</span>,s + a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].z;</span><br><span class="line">        As[<span class="number">0</span>][<span class="built_in">OFFSET</span>(a_copy_x*<span class="number">4</span> + <span class="number">3</span>,s + a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].w;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt; b_k; s+=b_copy_stride)&#123;</span><br><span class="line">        <span class="comment">//1. 全局内存-&gt;共享内存</span></span><br><span class="line">        <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="number">0</span>][<span class="built_in">OFFSET</span>(s + b_copy_y, <span class="number">4</span>*b_copy_x, b_n)]) = <span class="built_in">FETCH_FLOAT4</span>(B[<span class="built_in">OFFSET</span>(s + b_copy_y, <span class="number">4</span>*b_copy_x, N)]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    A += b_k;</span><br><span class="line">    B += b_k*N;</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 class="comment">//以上是全局内存-&gt;寄存器-&gt;共享内存（写共享内存）</span></span><br><span class="line">    __syncthreads();</span><br><span class="line">    <span class="comment">//因为是不同thread来写共享内存，所以必须要有sync</span></span><br><span class="line">    <span class="comment">//以下是共享内存-&gt;寄存器（读共享内存）</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//【共享内存-&gt;寄存器缓冲区】2. 先从a_tile(0)中，拷贝l=0时刻的所需要的共享内存值</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m+=<span class="number">4</span>)&#123;</span><br><span class="line">        <span class="built_in">FETCH_FLOAT4</span>(a_reg_l[<span class="number">0</span>][m]) = <span class="built_in">FETCH_FLOAT4</span>(As[<span class="number">0</span>][<span class="built_in">OFFSET</span>(<span class="number">0</span>, ty*t_m + m, b_m)]);</span><br><span class="line">    &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> n=<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">        <span class="built_in">FETCH_FLOAT4</span>(b_reg_l[<span class="number">0</span>][n]) = <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="number">0</span>][<span class="built_in">OFFSET</span>(<span class="number">0</span>, tx*t_n + n, b_n)]);</span><br><span class="line">    &#125;</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"></span><br><span class="line">    <span class="comment">//【BK,2BK, ..., K】【共K次循环】【以计算为计数点】</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> k = b_k; k&lt;K+b_k; k+=b_k)&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//更新shared_index为下一个要写入的共享内存缓冲区</span></span><br><span class="line">        shared_index = <span class="number">1</span> - shared_index;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//【BK,2BK,...,K-BK】【共K-1次循环】</span></span><br><span class="line">        <span class="keyword">if</span>(k&lt;K)&#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//1. 启动全局内存-&gt;寄存器【拷贝下一个A_tile, B_tile】</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">0</span>; s&lt;b_m; s+=a_copy_stride)&#123;</span><br><span class="line">                <span class="comment">//1. 全局内存-&gt;寄存器</span></span><br><span class="line">                A_tile_src[s/a_copy_stride] = <span class="built_in">FETCH_FLOAT4</span>(A[<span class="built_in">OFFSET</span>(s+a_copy_y, <span class="number">4</span>*a_copy_x, K)]);</span><br><span class="line">            &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">                <span class="comment">//1. 全局内存-&gt;寄存器</span></span><br><span class="line">                B_tile_src[s/b_copy_stride] = <span class="built_in">FETCH_FLOAT4</span>(B[<span class="built_in">OFFSET</span>(s+b_copy_y, <span class="number">4</span>*b_copy_x, N)]);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            A += b_k;</span><br><span class="line">            B += b_k*N;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">        * ！！！！在这个过程中，发生指令重叠</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">//【计算过程】</span></span><br><span class="line">        <span class="comment">//l:【0,1,...,b_k-2】</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> l = <span class="number">0</span>; l&lt;b_k - <span class="number">1</span>; l++)&#123;</span><br><span class="line">            <span class="comment">//共享内存-&gt;寄存器缓冲区</span></span><br><span class="line">            register_index = <span class="number">1</span> - register_index; <span class="comment">//指向这次需要拷贝到的寄存器缓冲区</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">//从共享内存读下一时刻的 行/列 -&gt;寄存器写缓冲区</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m = <span class="number">0</span>; m&lt;t_m; m+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(a_reg_l[register_index][m]) = <span class="built_in">FETCH_FLOAT4</span>(As[<span class="number">1</span> - shared_index][<span class="built_in">OFFSET</span>(l<span class="number">+1</span>,ty*t_m +m,b_m)]);</span><br><span class="line">            &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n = <span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(b_reg_l[register_index][n]) = <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="number">1</span> - shared_index][<span class="built_in">OFFSET</span>(l<span class="number">+1</span>,tx*t_n + n,b_n)]);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            <span class="comment">//寄存器读缓冲区（当前时刻的寄存器值）计算</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m++)&#123;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n++)&#123;</span><br><span class="line">                    tmp[m][n] += a_reg_l[<span class="number">1</span> - register_index][m] * b_reg_l[<span class="number">1</span>-register_index][n];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment">//此时l = b_k-1, 拷贝下一个a_tile，btile的第一个l=0， 以及计算l=b_k-1</span></span><br><span class="line">        register_index = <span class="number">1</span>- register_index;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//除了最后一趟不需要访问下一个a_tile，b_tile</span></span><br><span class="line">        <span class="comment">//【BK,2BK,....,K-BK】【K-1次循环】</span></span><br><span class="line">        <span class="keyword">if</span>(k &lt; K )&#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//1. 启动寄存器-&gt;共享内存</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_m; s+=a_copy_stride)&#123;</span><br><span class="line">                As[shared_index][<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x, s+a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].x;</span><br><span class="line">                As[shared_index][<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">1</span>, s+a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].y;</span><br><span class="line">                As[shared_index][<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">2</span>, s+a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].z;</span><br><span class="line">                As[shared_index][<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">3</span>, s+a_copy_y, b_m)] = A_tile_src[s/a_copy_stride].w;</span><br><span class="line">            &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> s=<span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">                <span class="comment">//寄存器到共享内存</span></span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(Bs[shared_index][<span class="built_in">OFFSET</span>(s+b_copy_y, <span class="number">4</span>*b_copy_x, b_n)]) = B_tile_src[s/b_copy_stride];</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//上面是写共享内存，必须全部写完，才能读共享内存</span></span><br><span class="line">            __syncthreads();</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(a_reg_l[register_index][m]) = <span class="built_in">FETCH_FLOAT4</span>(As[shared_index][<span class="built_in">OFFSET</span>(<span class="number">0</span>, ty*t_m + m, b_m)]);</span><br><span class="line">            &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(b_reg_l[register_index][n]) = <span class="built_in">FETCH_FLOAT4</span>(Bs[shared_index][<span class="built_in">OFFSET</span>(<span class="number">0</span>, tx*t_n + n, b_n)]);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">       </span><br><span class="line">        <span class="comment">//最后一次计算当前tile的内容</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> m= <span class="number">0</span>; m&lt;t_m; m++)&#123;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n++)&#123;</span><br><span class="line">                tmp[m][n] += a_reg_l[<span class="number">1</span> - register_index][m] * b_reg_l[<span class="number">1</span>-register_index][n];</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//对于C_tile，tmp的每一行</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m; m++)&#123;</span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> n=<span class="number">0</span>; n&lt;t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">            float4 C_temp = <span class="built_in">FETCH_FLOAT4</span>(C[<span class="built_in">OFFSET</span>(m,n,N)]);</span><br><span class="line">            C_temp.x = alpha*tmp[m][n] + beta*C_temp.x;</span><br><span class="line">            C_temp.y = alpha*tmp[m][n<span class="number">+1</span>] + beta*C_temp.y;</span><br><span class="line">            C_temp.z = alpha*tmp[m][n<span class="number">+2</span>] + beta*C_temp.z;</span><br><span class="line">            C_temp.w = alpha*tmp[m][n<span class="number">+3</span>] + beta*C_temp.w;</span><br><span class="line">            <span class="built_in">FETCH_FLOAT4</span>(C[<span class="built_in">OFFSET</span>(m,n,N)]) = C_temp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><mark style="background:#40a9ff">可以看到，这回根据自己的理解写的V5_2，性能已经比老师的要好了</mark><br><img src="/images/Pasted%20image%2020260528111329.png"></p><h3 id="优化：warp-tiling"><a href="#优化：warp-tiling" class="headerlink" title="优化：warp tiling"></a>优化：warp tiling</h3><p>下面进一步进行优化</p><blockquote><p>这里的warptile, 我说一下我的理解：</p></blockquote><p>   原来我们一个block, 负责计算C_sub（b_m x b_n）, 然后<br>   里面所有的线程，负责计算C_tile(t_m x t_n) .</p><blockquote><p>但是，当时所有的thread，都是执行一样的流程，处理各自的C_tile，所以，理想情况，一个thread阻塞，所有thread都阻塞。调度的整体是block。 </p><p>因为GPU里面是warp &#x3D; 32个thread为调度单位的，所以，<br>我们以32个thread为一个warp来重新划分C_tile，所以现在划分C_tile (w_m x w_n), 这样不同warp的C_tile就可以进行调度了。 </p><p><strong>每个warp负责的C_tile(w_m x w_n)</strong>，里面是32个thread来处理的，</p><p>这个C_tile内部又划分<strong>C_tile_sub(w_sub_m x w_sub_n)</strong>(迭代单位)，所有C_tile_sub都是32个线程来处理进行迭代的。32个thread一起处理一个C_tile_sub, 处理完这个C_tile_sub， 迭代处理下一个C_tile_sub，</p><p>所以一共迭代WMITER x WNITER次，每个thread针对一个C_tile_sub内的一个自己的小块(t_m x t_n), 32个thread共同计算一个C_tile_sub。</p></blockquote><p>你现在的主线理解，可以概括成：</p><ol><li><strong>一个 block 先负责一个大的输出块</strong> <code>C_sub = BM × BN</code></li><li>这个 <code>C_sub</code> <strong>再分给多个 warp</strong></li><li><strong>每个 warp 负责一个更小的输出块</strong> <code>warp tile = WM × WN</code></li><li>但是一个 warp 一次也不会直接把整个 <code>WM × WN</code> 全算完</li><li>所以 <code>WM × WN</code> <strong>还要继续分成多个阶段性的小块</strong><br> <code>WSUBM × WSUBN</code></li><li>每次迭代里，<strong>32 个线程共同处理一个 <code>WSUBM × WSUBN</code></strong></li><li>而在这个 <code>WSUBM × WSUBN</code> 内部，<strong>每个线程再负责自己的 <code>TM × TN</code> 微块</strong></li><li>所以 warp 需要迭代：<ul><li><code>WMITER = WM / WSUBM</code></li><li><code>WNITER = WN / WSUBN</code></li><li>一共 <code>WMITER × WNITER</code> 次，才能完成整个 <code>WM × WN</code></li></ul></li></ol><p>这个主线是 <strong>对的</strong>。</p><p><mark style="background:#affad1">warp tiling 真正带来的好处是</mark>：</p><p><img src="/images/Pasted%20image%2020260528111931.png" alt="531"></p><p><img src="/images/Pasted%20image%2020260528111938.png" alt="341"></p><p><img src="/images/Pasted%20image%2020260528111945.png" alt="443"></p><p><mark style="background:#affad1">所以最后总结为</mark><br><img src="/images/Pasted%20image%2020260528112139.png"></p><p><img src="/images/Pasted%20image%2020260528112236.png" alt="506"></p><p><img src="/images/Pasted%20image%2020260528112247.png"><br><img src="/images/Pasted%20image%2020260528112300.png"></p><p>那thread划分好了，那整个矩阵运算，其实就是两个大的部分：</p><ul><li>全局内存-&gt;共享内存（拷贝A_tile, B_tile）</li><li>共享内存-&gt;寄存器（拷贝l步的As的t_m行，Bs的t_n列）</li></ul><p>那么在warp tiling优化中，<mark style="background:#fff88f">第一个全局内存 -&gt; 共享内存是没有变化的</mark>。<br><img src="/images/Pasted%20image%2020260528113233.png"></p><p>真正的变化是<mark style="background:#fff88f">共享内存-&gt;寄存器这一步，多了C_tile内的C_tile_sub迭代这一个细粒度</mark><br><img src="/images/Pasted%20image%2020260528113253.png"><br><img src="/images/Pasted%20image%2020260528113302.png"></p><p><img src="/images/Pasted%20image%2020260528113331.png" alt="255"></p><p><img src="/images/67794b5dd69ce62b9e51b98c68433b44.jpg"></p><p><img src="/images/b24d7b6a4e7021fc89cc114796579623.jpg"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * V6: 用warp tiling来优化，配合硬件的调度模式，软件的工作分配也以warp来进行</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">constexpr</span> <span class="type">int</span> WARP_SIZE = <span class="number">32</span>;</span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m, <span class="type">const</span> <span class="type">int</span> b_n, <span class="type">const</span> <span class="type">int</span> b_k, <span class="comment">//每个C_sub的性质</span></span></span><br><span class="line"><span class="function">        <span class="type">const</span> <span class="type">int</span> w_m, <span class="type">const</span> <span class="type">int</span> w_n, <span class="comment">//每个C_tile的性质</span></span></span><br><span class="line"><span class="function">        <span class="type">const</span> <span class="type">int</span> w_n_iter_num, </span></span><br><span class="line"><span class="function">        <span class="type">const</span> <span class="type">int</span> t_m, <span class="type">const</span> <span class="type">int</span> t_n, <span class="type">const</span> <span class="type">int</span> num_threads &gt;</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">mysgemm_v6</span><span class="params">(<span class="type">int</span> M, <span class="type">int</span> N, <span class="type">int</span> K, <span class="type">float</span> alpha, <span class="type">float</span>* A, <span class="type">float</span>* B, <span class="type">float</span> beta, <span class="type">float</span>* C)</span></span>&#123;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//C_sub,A_sub,B_sub</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_y = blockIdx.y;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_x = blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//C_tile的坐标</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> warp_id = threadIdx.x / WARP_SIZE;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> w_y = warp_id / (b_n / w_n);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> w_x = warp_id % (b_n / w_n);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * WARP_SIZE * TM * TN = 一个C_tile_sub 的float数</span></span><br><span class="line"><span class="comment">     * WARP_SIZE * TM * TN * WNITER = 一行C_tile_sub 的float数</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="comment">//一个C_tile有w_m_iter_num行C_tile_sub</span></span><br><span class="line">    <span class="comment">//一个C_tile有w_n_iter_num列C_tile_sub</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> w_m_iter_num = (w_m * w_n) / (WARP_SIZE*t_m*t_n * w_n_iter_num);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> w_sub_m = w_m / w_m_iter_num;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> w_sub_n = w_n / w_n_iter_num;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//该thread在一个C_tile_sub中的坐标</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> thread_id_in_c_tile_sub = threadIdx.x % WARP_SIZE;</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> ty = thread_id_in_c_tile_sub /(w_sub_n / t_n);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> tx = thread_id_in_c_tile_sub %(w_sub_n / t_n);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//共享内存</span></span><br><span class="line">    __shared__ <span class="type">float</span> As[b_k * b_m];</span><br><span class="line">    __shared__ <span class="type">float</span> Bs[b_k * b_n];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//重定向A-&gt;A_sub, B-&gt;B_sub, C-&gt;C_tile(warp)</span></span><br><span class="line">    A = &amp;A[<span class="built_in">OFFSET</span>(b_y * b_m, <span class="number">0</span>, K)];</span><br><span class="line">    B = &amp;B[<span class="built_in">OFFSET</span>(<span class="number">0</span>,b_x * b_n,N)];</span><br><span class="line"></span><br><span class="line">    C = &amp;C[<span class="built_in">OFFSET</span>(b_y * b_m + w_y * w_m,   b_x * b_n + w_x*w_n,   N)];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//【全局内存-&gt;共享内存】【一次拷贝4个float的坐标】</span></span><br><span class="line">    <span class="comment">// 这个阶段和warp划分无关</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> a_copy_y = threadIdx.x / (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> a_copy_x = threadIdx.x % (b_k / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> a_copy_stride = num_threads / (b_k / <span class="number">4</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_copy_y = threadIdx.x / (b_n / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_copy_x = threadIdx.x % (b_n / <span class="number">4</span>);</span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> b_copy_stride = num_threads / (b_n / <span class="number">4</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//每个thread的t_m, t_n的计算中间结果（对于每个l）</span></span><br><span class="line">    <span class="type">float</span> thread_result[t_m][t_n][w_m_iter_num][w_n_iter_num] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//计算(t_m, t_n)中每个float元素所需的Mp的一行，Np的一列的寄存器缓冲区</span></span><br><span class="line">    <span class="type">float</span> a_reg_l[w_m_iter_num * t_m] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line">    <span class="type">float</span> b_reg_l[w_n_iter_num * t_n] = &#123;<span class="number">0.0f</span>&#125;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> k =<span class="number">0</span>; k&lt;K; k+= b_k)&#123;</span><br><span class="line">        <span class="comment">//针对每一个A_tile, B_tile切片</span></span><br><span class="line">        <span class="comment">//1. 【全局内存-&gt;共享内存】（一整个warp所需）</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s = <span class="number">0</span>; s&lt;b_m; s+=a_copy_stride)&#123;</span><br><span class="line">            <span class="comment">//全局内存-&gt;寄存器</span></span><br><span class="line">            float4 src = <span class="built_in">FETCH_FLOAT4</span>(A[<span class="built_in">OFFSET</span>(s + a_copy_y, <span class="number">4</span>*a_copy_x,K)]);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">//寄存器-&gt;转置共享内存</span></span><br><span class="line">            As[<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x, a_copy_y + s,b_m)] = src.x;</span><br><span class="line">            As[<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">1</span>, a_copy_y + s,b_m)] = src.y;</span><br><span class="line">            As[<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">2</span>, a_copy_y + s,b_m)] = src.z;</span><br><span class="line">            As[<span class="built_in">OFFSET</span>(<span class="number">4</span>*a_copy_x + <span class="number">3</span>, a_copy_y + s,b_m)] = src.w;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> s =<span class="number">0</span>; s&lt;b_k; s+=b_copy_stride)&#123;</span><br><span class="line">            <span class="built_in">FETCH_FLOAT4</span>(Bs[<span class="built_in">OFFSET</span>(s + b_copy_y, <span class="number">4</span>*b_copy_x,b_n)]) = </span><br><span class="line">                <span class="built_in">FETCH_FLOAT4</span>(B[<span class="built_in">OFFSET</span>(s+b_copy_y, <span class="number">4</span>*b_copy_x, N)]);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        A += b_k;</span><br><span class="line">        B += b_k*N;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment">//因为上面写共享内存，下面用共享内存，所以需要sync</span></span><br><span class="line">        __syncthreads();</span><br><span class="line">        <span class="comment">//2. 【共享内存-&gt;寄存器+计算】</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> l = <span class="number">0</span>; l &lt; b_k; l++)&#123;</span><br><span class="line">            <span class="comment">//【共享内存-&gt;寄存器缓冲区】【w_m_iter_num*t_m】</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> iter=<span class="number">0</span>; iter &lt; w_m_iter_num; iter++)&#123;</span><br><span class="line">                <span class="comment">//这边不再向量化访问共享内存了，因为t_m细粒化程度太高，可能不够4个float，所以老老实实挨个拷贝</span></span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m&lt;t_m;m++)&#123;</span><br><span class="line">                    a_reg_l[<span class="built_in">OFFSET</span>(iter,m,t_m)] = As[<span class="built_in">OFFSET</span>(l,w_y*w_m + iter*w_sub_m + ty*t_m + m, b_m)];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//【w_n_iter_num*t_n】</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> iter=<span class="number">0</span>; iter &lt; w_n_iter_num; iter++)&#123;</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n&lt;t_n; n++)&#123;</span><br><span class="line">                    b_reg_l[<span class="built_in">OFFSET</span>(iter,n,t_n)] = Bs[<span class="built_in">OFFSET</span>(l,w_x*w_n + iter*w_sub_n + tx*t_n + n,b_n)];</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//【计算】【(t_m*t_n) * (w_m_iter_num*w_n_iter_num) 】</span></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> iter_y =<span class="number">0</span>; iter_y &lt; w_m_iter_num; iter_y++)&#123;</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> iter_x =<span class="number">0</span>; iter_x &lt; w_n_iter_num; iter_x++)&#123;</span><br><span class="line">                    <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">                        <span class="keyword">for</span>(<span class="type">int</span> n =<span class="number">0</span>; n &lt; t_n; n++)&#123;</span><br><span class="line">                            thread_result[m][n][iter_y][iter_x] += a_reg_l[<span class="built_in">OFFSET</span>(iter_y,m,t_m)] * b_reg_l[<span class="built_in">OFFSET</span>(iter_x,n,t_n)];</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//读完共享内存也要全部sync后才能进入下一个loop来写共享内存</span></span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//写回全局内存</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> iter_y = <span class="number">0</span>; iter_y &lt; w_m_iter_num; iter_y++)&#123;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> iter_x =<span class="number">0</span>; iter_x &lt; w_n_iter_num; iter_x++)&#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//针对一个(t_m x t_n)</span></span><br><span class="line">            <span class="type">float</span>* C_write = &amp;C[<span class="built_in">OFFSET</span>(iter_y*w_sub_m, iter_x*w_sub_n,N)];</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> m =<span class="number">0</span>; m &lt; t_m; m++)&#123;</span><br><span class="line">                <span class="comment">//【这里假定t_n是4的倍数，可以向量化加载】</span></span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> n=<span class="number">0</span>; n&lt; t_n; n+=<span class="number">4</span>)&#123;</span><br><span class="line">                    float4 temp = <span class="built_in">FETCH_FLOAT4</span>(C_write[<span class="built_in">OFFSET</span>(ty*t_m + m, tx*t_n + n, N)]);</span><br><span class="line">                    temp.x = alpha*thread_result[m][n][iter_y][iter_x]  + beta*temp.x;</span><br><span class="line">                    temp.y = alpha*thread_result[m][n<span class="number">+1</span>][iter_y][iter_x]  + beta*temp.y;</span><br><span class="line">                    temp.z = alpha*thread_result[m][n<span class="number">+2</span>][iter_y][iter_x]  + beta*temp.z;</span><br><span class="line">                    temp.w = alpha*thread_result[m][n<span class="number">+3</span>][iter_y][iter_x]  + beta*temp.w;</span><br><span class="line">                    <span class="built_in">FETCH_FLOAT4</span>(C_write[<span class="built_in">OFFSET</span>(ty*t_m + m,tx*t_n + n,N)]) = temp;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看到，效果非常的nice。<br><img src="/images/Pasted%20image%2020260528152139.png"></p><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p>至此，我们总结一下目前学到的优化策略：</p><ul><li><strong>共享内存优化</strong><ul><li>一口气多读一些全局内存到共享内存，然后这部分计算用共享内存</li><li><img src="/images/Pasted%20image%2020260528153111.png" alt="461"></li></ul></li><li><strong>向量化访存</strong><ul><li>优化访存密度，原来一次访问全局内存一个float的时间 &#x3D; 访问全局内存4个float的时间</li><li><img src="/images/Pasted%20image%2020260528153151.png" alt="544"></li><li><img src="/images/Pasted%20image%2020260528153216.png" alt="509"></li></ul></li><li><strong>thread tiling(warp tiling的简易版) + 共享内存转置</strong><ul><li>共享内存转置：（利用计算访存的特性，减少bank conflict）</li><li><img src="/images/Pasted%20image%2020260528153520.png" alt="427"></li><li>把thread数和要计算的float元素分离，一个thread计算多个float元素，计算效率上升，GFLOPs提高（这叫什么？计算密度？也就是单位时间内发生的浮点数计算次数？）</li><li><img src="/images/Pasted%20image%2020260528153257.png" alt="215"><img src="/images/Pasted%20image%2020260528153407.png" alt="255"></li></ul></li><li><strong>双缓冲区</strong><ul><li>利用双缓冲区+指令重叠（FMA指令执行上下不依赖），空间换时间</li><li><img src="/images/Pasted%20image%2020260528153550.png" alt="421"></li></ul></li><li><strong>warp tiling</strong><ul><li>按照GPU的warp调度的特性，来划分计算的工作，所以，一个block确定后，先按照warp来分配工作量（C_tile），然后里面用32个thread(一个warp)来处理，同时里面根据warp调度的特性，划分成各个迭代分区C_tile_sub, 然后每个C_tile_sub内，再划分出32个thread各自的(t_m, t_n)的工作任务。</li><li><img src="/images/Pasted%20image%2020260528153616.png" alt="445"></li></ul></li></ul><h2 id="Cuda原子操作"><a href="#Cuda原子操作" class="headerlink" title="Cuda原子操作"></a>Cuda原子操作</h2><p>就是我们CUDA编程，是并行计算，多个thread一起工作，当他们操作一个共享内存的时候，就容易出现并发与争抢的情况。</p><p>所以需要原子操作，保证指令级不会被中断。</p><p><img src="/images/Pasted%20image%2020260528160951.png"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;cuda_runtime.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">race_condition_kernel</span><span class="params">(<span class="type">int</span>* data)</span></span>&#123;</span><br><span class="line">    <span class="comment">// int temp = *data;</span></span><br><span class="line">    <span class="comment">// temp = temp + 1;</span></span><br><span class="line">    <span class="comment">// *data = temp;</span></span><br><span class="line">    <span class="built_in">atomicAdd</span>(data, <span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">hist</span><span class="params">(<span class="type">int8_t</span> *input, <span class="type">int</span> *hist, <span class="type">int</span> n)</span></span>&#123;</span><br><span class="line">    <span class="type">int</span> i = threadIdx.x + blockIdx.x*blockDim.x; <span class="comment">//计算该thread在grid中的id</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//以所有thread为一个stride，每个stride里面，一个thread读一个</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> idx = i; idx &lt; n; idx +=gridDim.x * blockDim.x)&#123;</span><br><span class="line">        <span class="type">int8_t</span> in = input[idx]; <span class="comment">//全局内存读取对应的位置</span></span><br><span class="line">        <span class="keyword">if</span>(in &gt;= <span class="number">0</span> &amp;&amp; in &lt;<span class="number">256</span>)&#123;</span><br><span class="line">            <span class="built_in">atomicAdd</span>(&amp;hist[in], <span class="number">1</span>);<span class="comment">//全局内存写</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">hist_v2</span><span class="params">(<span class="type">int8_t</span> *input, <span class="type">int</span>*hist, <span class="type">int</span> n)</span></span>&#123;</span><br><span class="line">    __shared__ <span class="type">int</span> histo_private[<span class="number">256</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//写共享内存</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> j = threadIdx.x; j&lt;<span class="number">256</span>; j+=blockDim.x)&#123;</span><br><span class="line">        histo_private[j] = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> i = threadIdx.x + blockIdx.x * blockDim.x;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> idx = i; idx &lt; n ; idx +=gridDim.x*blockDim.x)&#123;</span><br><span class="line">        <span class="comment">//访问全局内存</span></span><br><span class="line">        <span class="type">int8_t</span> in = input[idx];</span><br><span class="line">    </span><br><span class="line">        <span class="comment">//写共享内存</span></span><br><span class="line">        <span class="keyword">if</span>(in &gt;=<span class="number">0</span> &amp;&amp; in &lt; <span class="number">256</span>)&#123;</span><br><span class="line">            <span class="built_in">atomicAdd</span>(&amp;histo_private[in], <span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line">    <span class="comment">//读共享内存，写全局内存</span></span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> j=threadIdx.x ; j&lt;<span class="number">256</span>; j+=blockDim.x)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">//每个thread负责拷贝一个int, 不够就循环拷贝</span></span><br><span class="line">        <span class="built_in">atomicAdd</span>(&amp;hist[j], histo_private[j]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// 用较大的数据量，让时间测量有意义</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> size = <span class="number">1024</span> * <span class="number">1024</span>; <span class="comment">// 1M 个元素</span></span><br><span class="line">    <span class="type">int8_t</span> *input = <span class="keyword">new</span> <span class="type">int8_t</span>[size];</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; size; i++)&#123;</span><br><span class="line">        input[i] = i % <span class="number">256</span>;  <span class="comment">// 值均匀分布在 0~255</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分配 host 直方图</span></span><br><span class="line">    <span class="type">int</span>* h_hist_v1 = <span class="keyword">new</span> <span class="type">int</span>[<span class="number">256</span>]();</span><br><span class="line">    <span class="type">int</span>* h_hist_v2 = <span class="keyword">new</span> <span class="type">int</span>[<span class="number">256</span>]();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分配 device 端</span></span><br><span class="line">    <span class="type">int8_t</span>* d_input;</span><br><span class="line">    <span class="type">int</span>* d_hist_v1;</span><br><span class="line">    <span class="type">int</span>* d_hist_v2;</span><br><span class="line">    <span class="built_in">cudaMalloc</span>(&amp;d_input, size * <span class="built_in">sizeof</span>(<span class="type">int8_t</span>));</span><br><span class="line">    <span class="built_in">cudaMalloc</span>(&amp;d_hist_v1, <span class="number">256</span> * <span class="built_in">sizeof</span>(<span class="type">int</span>));</span><br><span class="line">    <span class="built_in">cudaMalloc</span>(&amp;d_hist_v2, <span class="number">256</span> * <span class="built_in">sizeof</span>(<span class="type">int</span>));</span><br><span class="line"></span><br><span class="line">    <span class="built_in">cudaMemcpy</span>(d_input, input, <span class="built_in">sizeof</span>(<span class="type">int8_t</span>) * size, cudaMemcpyHostToDevice);</span><br><span class="line"></span><br><span class="line">    <span class="function">dim3 <span class="title">block_size</span><span class="params">(<span class="number">256</span>)</span></span>;</span><br><span class="line">    <span class="function">dim3 <span class="title">grid_size</span><span class="params">(<span class="number">256</span>)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建 CUDA event 用于计时</span></span><br><span class="line">    cudaEvent_t start, stop;</span><br><span class="line">    <span class="built_in">cudaEventCreate</span>(&amp;start);</span><br><span class="line">    <span class="built_in">cudaEventCreate</span>(&amp;stop);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ===== 测试 hist (全局内存版本) =====</span></span><br><span class="line">    <span class="built_in">cudaMemset</span>(d_hist_v1, <span class="number">0</span>, <span class="number">256</span> * <span class="built_in">sizeof</span>(<span class="type">int</span>));</span><br><span class="line">    <span class="built_in">cudaEventRecord</span>(start);</span><br><span class="line">    hist&lt;&lt;&lt;grid_size, block_size&gt;&gt;&gt;(d_input, d_hist_v1, size);</span><br><span class="line">    <span class="built_in">cudaEventRecord</span>(stop);</span><br><span class="line">    <span class="built_in">cudaEventSynchronize</span>(stop);</span><br><span class="line">    <span class="type">float</span> ms_v1 = <span class="number">0</span>;</span><br><span class="line">    <span class="built_in">cudaEventElapsedTime</span>(&amp;ms_v1, start, stop);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">cudaDeviceSynchronize</span>();</span><br><span class="line">    <span class="built_in">cudaMemcpy</span>(h_hist_v1, d_hist_v1, <span class="built_in">sizeof</span>(<span class="type">int</span>) * <span class="number">256</span>, cudaMemcpyDeviceToHost);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ===== 测试 hist_v2 (共享内存版本) =====</span></span><br><span class="line">    <span class="built_in">cudaMemset</span>(d_hist_v2, <span class="number">0</span>, <span class="number">256</span> * <span class="built_in">sizeof</span>(<span class="type">int</span>));</span><br><span class="line">    <span class="built_in">cudaEventRecord</span>(start);</span><br><span class="line">    hist_v2&lt;&lt;&lt;grid_size, block_size&gt;&gt;&gt;(d_input, d_hist_v2, size);</span><br><span class="line">    <span class="built_in">cudaEventRecord</span>(stop);</span><br><span class="line">    <span class="built_in">cudaEventSynchronize</span>(stop);</span><br><span class="line">    <span class="type">float</span> ms_v2 = <span class="number">0</span>;</span><br><span class="line">    <span class="built_in">cudaEventElapsedTime</span>(&amp;ms_v2, start, stop);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">cudaDeviceSynchronize</span>();</span><br><span class="line">    <span class="built_in">cudaMemcpy</span>(h_hist_v2, d_hist_v2, <span class="built_in">sizeof</span>(<span class="type">int</span>) * <span class="number">256</span>, cudaMemcpyDeviceToHost);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ===== 比较结果 =====</span></span><br><span class="line">    <span class="type">bool</span> correct = <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">256</span>; i++)&#123;</span><br><span class="line">        <span class="keyword">if</span>(h_hist_v1[i] != h_hist_v2[i])&#123;</span><br><span class="line">            cout &lt;&lt; <span class="string">&quot;MISMATCH at bin &quot;</span> &lt;&lt; i &lt;&lt; <span class="string">&quot;: v1=&quot;</span> &lt;&lt; h_hist_v1[i] &lt;&lt; <span class="string">&quot; v2=&quot;</span> &lt;&lt; h_hist_v2[i] &lt;&lt; endl;</span><br><span class="line">            correct = <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(correct)&#123;</span><br><span class="line">        cout &lt;&lt; <span class="string">&quot;Result: PASS - both kernels produce identical histograms&quot;</span> &lt;&lt; endl;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        cout &lt;&lt; <span class="string">&quot;Result: FAIL - histograms differ&quot;</span> &lt;&lt; endl;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; endl;</span><br><span class="line">    cout &lt;&lt; <span class="string">&quot;hist    (global atomic): &quot;</span> &lt;&lt; ms_v1 &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; endl;</span><br><span class="line">    cout &lt;&lt; <span class="string">&quot;hist_v2 (shared atomic): &quot;</span> &lt;&lt; ms_v2 &lt;&lt; <span class="string">&quot; ms&quot;</span> &lt;&lt; endl;</span><br><span class="line">    cout &lt;&lt; <span class="string">&quot;Speedup: &quot;</span> &lt;&lt; ms_v1 / ms_v2 &lt;&lt; <span class="string">&quot;x&quot;</span> &lt;&lt; endl;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打印前 16 个 bin 作为预览</span></span><br><span class="line">    cout &lt;&lt; endl &lt;&lt; <span class="string">&quot;First 16 bins (should be &quot;</span> &lt;&lt; size / <span class="number">256</span> &lt;&lt; <span class="string">&quot; each):&quot;</span> &lt;&lt; endl;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">16</span>; i++)&#123;</span><br><span class="line">        cout &lt;&lt; <span class="string">&quot;  bin &quot;</span> &lt;&lt; i &lt;&lt; <span class="string">&quot;: v1=&quot;</span> &lt;&lt; h_hist_v1[i] &lt;&lt; <span class="string">&quot;  v2=&quot;</span> &lt;&lt; h_hist_v2[i] &lt;&lt; endl;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 清理</span></span><br><span class="line">    <span class="built_in">cudaEventDestroy</span>(start);</span><br><span class="line">    <span class="built_in">cudaEventDestroy</span>(stop);</span><br><span class="line">    <span class="built_in">cudaFree</span>(d_hist_v1);</span><br><span class="line">    <span class="built_in">cudaFree</span>(d_hist_v2);</span><br><span class="line">    <span class="built_in">cudaFree</span>(d_input);</span><br><span class="line">    <span class="keyword">delete</span>[] input;</span><br><span class="line">    <span class="keyword">delete</span>[] h_hist_v1;</span><br><span class="line">    <span class="keyword">delete</span>[] h_hist_v2;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">Result: PASS - both kernels produce identical histograms</span><br><span class="line"></span><br><span class="line">hist    (global atomic): 0.022528 ms</span><br><span class="line">hist_v2 (shared atomic): 0.009216 ms</span><br><span class="line">Speedup: 2.44444x</span><br></pre></td></tr></table></figure><h2 id="Transpose转置算子，数据排布优化"><a href="#Transpose转置算子，数据排布优化" class="headerlink" title="Transpose转置算子，数据排布优化"></a>Transpose转置算子，数据排布优化</h2><p>这里主要来解释当初的共享内存访问问题，为什么需要转置写。<br>这里就是主要讲解bank conflict问题。</p><p><strong>不同thread访问不同地址但同一bank，会出现bank conflict</strong><br><img src="/images/Pasted%20image%2020260528192313.png"></p><p>注意，访问同一地址，不会构成bank conflict<br><img src="/images/Pasted%20image%2020260528192511.png"></p><h3 id="朴素实现"><a href="#朴素实现" class="headerlink" title="朴素实现"></a>朴素实现</h3><p><img src="/images/Pasted%20image%2020260528194816.png"></p><p><img src="/images/Pasted%20image%2020260528194809.png"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">仅使用全局显存的转置kernel</span></span><br><span class="line"><span class="comment">朴素实现</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OFFSET(row, col, ld) ((row)*(ld) + (col))</span></span><br><span class="line"><span class="function">__global__ <span class="type">void</span> <span class="title">naiveGmem</span><span class="params">(<span class="type">float</span>* out, <span class="type">float</span>* in, <span class="type">int</span> nx, <span class="type">int</span> ny)</span></span>&#123;</span><br><span class="line">    <span class="comment">//应该是一个thread负责拷贝一个float元素</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//计算该线程在grid内的x轴的全局索引</span></span><br><span class="line">    <span class="type">int</span> ix = blockDim.x * blockIdx.x + threadIdx.x;</span><br><span class="line">    <span class="type">int</span> iy = blockDim.y * blockIdx.y + threadIdx.y;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//【这里有全局内存读，全局内存写】</span></span><br><span class="line">    <span class="comment">//【全局内存读：是合并操作，相邻线程读入的地址是连续的】</span></span><br><span class="line">    <span class="comment">//【全局内存写：是非合并操作，相邻线程写入的地址是非连续的，是写入一列】</span></span><br><span class="line">    <span class="keyword">if</span>(iy &lt; ny &amp;&amp; ix &lt; nx)&#123;</span><br><span class="line">        out[<span class="built_in">OFFSET</span>(ix,iy,ny)] = in[<span class="built_in">OFFSET</span>(iy, ix, nx)];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">call_naiveGmem</span><span class="params">(<span class="type">float</span>*d_out, <span class="type">float</span>*d_in, <span class="type">int</span> nx, <span class="type">int</span> ny)</span></span>&#123;</span><br><span class="line">    <span class="function">dim3 <span class="title">blockSize</span><span class="params">(<span class="number">16</span>,<span class="number">16</span>)</span></span>;</span><br><span class="line">    <span class="function">dim3 <span class="title">gridSize</span><span class="params">((nx + blockSize.x - <span class="number">1</span>)/blockSize.x,</span></span></span><br><span class="line"><span class="params"><span class="function">                (ny + blockSize.y - <span class="number">1</span>)/blockSize.y)</span></span>;</span><br><span class="line">    naiveGmem&lt;&lt;&lt;gridSize, blockSize&gt;&gt;&gt;(d_out, d_in, nx, ny);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从Nsight分析来看，计算效率很低，只有10%不到，说明执行的thread的大部分时间都是在阻塞等待全局内存的数据的读写。尤其是<strong>非合并写入</strong></p><p><img src="/images/Pasted%20image%2020260528194948.png"></p><p><strong>注意点：</strong></p><p><img src="/images/Pasted%20image%2020260528203041.png" alt="377"></p><ol><li><strong>我们这里的转置任务，特地配置了总的thread数量是等于nx * ny的，所以一个thread只需要拷贝它对应iy, ix 处的float即可。</strong></li></ol><p><img src="/images/Pasted%20image%2020260528203205.png"></p><p>所以这里的<strong>主要问题</strong>有两个：</p><ul><li>没有共享显存的加速</li><li><mark style="background:#ff4d4f">非合并的写入</mark><ul><li><strong>我们的相邻线程是ix是连续的，iy是不连续的</strong>，所以变动频繁的是ix<ul><li>所以，读全局显存的时候，ix没有乘系数，所以可以<strong>合并访问</strong></li><li>写全局显存的时候，ix乘了系数，所以对out这个全局显存的访问跨度是很大的，所以<strong>不是合并访问</strong></li></ul></li></ul></li></ul><p><strong>所以，相邻线程，ix变动频繁，会导致我们的写入全局内存是非合并的。</strong></p><p><mark style="background:#affad1">全局显存的合并写入&#x2F;非合并写入</mark></p><p><strong>合并访问</strong>：<br>加入一个warp的32个线程，一共能访问128个字节。<br><img src="/images/Pasted%20image%2020260528203800.png" alt="223"></p><p>此时就可以做一个合并的访问，把这32个请求同步发给全局显存的设备<br><img src="/images/Pasted%20image%2020260528203832.png" alt="159"><br><strong>哪怕他是在128字节范围内随机访问，也可以合并访问</strong></p><p><strong>非合并访问</strong><br>32个线程，每次访问的跨度，远远大于128字节。<br><img src="/images/Pasted%20image%2020260528204000.png" alt="306"></p><p>这种情况下，没有办法合并，显存的利用率非常的低下，就<strong>只能挨个发射访问请求</strong>， 这个就是上面的out的写入，因为ix变化平凡，所以没办法合并优化。</p><p>所以朴素实现，它的瓶颈在于：<mark style="background:#ff4d4f">drain stall</mark></p><p>就是说，每个kernel的thread最后一句指令执行完了，还要等到才能退出。因为全局内存写入。</p><h3 id="优化：用共享显存"><a href="#优化：用共享显存" class="headerlink" title="优化：用共享显存"></a>优化：用共享显存</h3><p>前面的朴素实现的问题在于，假设我们已知在原来的A中的坐标是（tx, ty）</p><p>直接的想法是在转置后的B中的坐标是(ty, tx)，但是这样会有一个问题。就是在写入out的时候，会导致对一维的显存写入，不能够合并访问。</p><p>所以，<strong>这里的思路就是</strong>：<br><mark style="background:#fff88f">每个线程，不是和float元素值绑死，而是和拷贝位置绑死。</mark></p><ul><li>原来朴素实现，每个线程是和值绑死的，thread读出的是3，就负责把这个3写入到全局内存的对应位置<ul><li>这就导致连续线程访问不连续的内存位置</li></ul></li><li>现在的共享显存优化：每个线程和内存位置绑死，thread只负责读&#x2F;写[4]这个位置，读的时候这个位置是3，写的时候这个位置可能就是2了，这就要去共享内存里面去获得这个值（转置之后的值）<br><img src="/images/4f038047b079e03bf7c666c1b12d3866.jpg" alt="408"></li></ul><p><img src="/images/Pasted%20image%2020260528210846.png"><br>所以，他的思路就是：</p><ul><li>我先从<strong>固定的顺序位置</strong>，拷贝当前的值，写入共享显存，<ul><li>上面写共享内存，下面读共享内存，所以需要sync</li></ul></li><li>然后从共享显存里面，找到别的thread写的，我接下来要写入<strong>固定顺序位置</strong>的值</li></ul><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * V2，用共享内存来解决非合并写入全局内存的问题</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="comment">//共享内存一次缓存一个tile (32 x 16)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BDIMX 32</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BDIMY 16</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m </span>= <span class="number">16</span>, <span class="type">const</span> <span class="type">int</span> b_n = <span class="number">32</span>&gt;</span><br><span class="line">__global__ <span class="type">void</span> <span class="built_in">transposeSmen</span>(<span class="type">float</span> *out, <span class="type">float</span> *in, <span class="type">const</span> <span class="type">int</span> nx, <span class="type">const</span> <span class="type">int</span> ny)&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//负责缓存整个block内的数据，每个thread都只负责这个block的顺序拷贝，不绑定特定的值</span></span><br><span class="line">    __shared__ <span class="type">float</span> tile[b_m][b_n];</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> M = ny;</span><br><span class="line">    <span class="type">int</span> N = nx;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//本thread在block内所有thread中的顺序。</span></span><br><span class="line">    <span class="type">int</span> t_id = <span class="built_in">OFFSET</span>(threadIdx.y, threadIdx.x,b_n);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//计算该thread需要拷贝输入block内的float坐标</span></span><br><span class="line">    <span class="type">int</span> t_y_in = threadIdx.y;</span><br><span class="line">    <span class="type">int</span> t_x_in = threadIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//计算该thread需要写入的输出block内的float坐标</span></span><br><span class="line">    <span class="type">int</span> t_y_out = t_id / b_m;</span><br><span class="line">    <span class="type">int</span> t_x_out = t_id % b_m;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> b_y = blockIdx.y;</span><br><span class="line">    <span class="type">int</span> b_x = blockIdx.x;</span><br><span class="line">    <span class="comment">//重定向in指向输入的block</span></span><br><span class="line">    in = &amp;in[<span class="built_in">OFFSET</span>(b_y * b_m, b_x * b_n,N)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//重定向out指向输出的block</span></span><br><span class="line">    out = &amp;out[<span class="built_in">OFFSET</span>(b_x*b_n, b_y*b_m,M)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//每个thread各自写入输入in中对应的位置。写入共享内存</span></span><br><span class="line">    <span class="comment">//连续thread，变动频繁的是t_x_in, 所以写入没有bank conflict</span></span><br><span class="line">    tile[t_y_in][t_x_in] = in[<span class="built_in">OFFSET</span>(t_y_in, t_x_in,N)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//等待所有thread写入共享内存完成</span></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//读共享内存，每个连续的线程，变动频繁的是t_x_out,此时会出现严重的bank conflict</span></span><br><span class="line">    out[<span class="built_in">OFFSET</span>(t_y_out, t_x_out, M)] = tile[t_x_out][t_y_out];</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="优化：共享内存（解决bank-conflict问题）（填充-破坏一维线性下标）"><a href="#优化：共享内存（解决bank-conflict问题）（填充-破坏一维线性下标）" class="headerlink" title="优化：共享内存（解决bank conflict问题）（填充&#x3D;破坏一维线性下标）"></a>优化：共享内存（解决bank conflict问题）（填充&#x3D;破坏一维线性下标）</h3><p>上面的共享内存优化，在最后一句，读共享内存的时候，频繁变动的是t_x_out, 所以实际上是竖着访问共享内存中的每一个数据的。</p><p><img src="/images/Pasted%20image%2020260529213208.png"></p><p>首先说明一下bank conflict的出现的原因：</p><ul><li>共享内存的硬件设计上，是32个bank，每个bank一次访问的带宽是一个float。</li><li>所以只有当你的共享内存的跨度是32个float&#x3D;128字节的时候，才会出现bank conflict的问题。</li></ul><blockquote><p>全局内存的合并写入的判定范围好像也是32个float</p></blockquote><blockquote><p><strong>所以，如果我的共享显存是5x5的形状，那么就算我竖着访问，也不会触发bank conflict。只有当共享显存的一行有32个float的时候，竖着访问就会触发bank conflict。</strong></p></blockquote><blockquote><p>注意，bank conflict本质上不是一列访问导致，而是在一维的索引上间隔32个导致</p></blockquote><p><mark style="background:#fff88f">解决办法</mark>：填充，来打破周期，不让两个线路的thread访问的位置在共享内存中出现在同一列。</p><p>那么它填充是什么办法呢？</p><p><mark style="background:#fff88f">所谓填充，其实是破环原来的一维索引下的32个间隔的关系，变成33个间隔。</mark> <mark style="background:#40a9ff">破坏的是线性下标</mark><br><img src="/images/Pasted%20image%2020260529223129.png" alt="342"><br><img src="/images/Pasted%20image%2020260529223140.png" alt="372"></p><p><img src="/images/Pasted%20image%2020260529223202.png" alt="324"><img src="/images/Pasted%20image%2020260529223228.png" alt="338"></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * V3：填充解决共享显存的的bank conflict</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m </span>= <span class="number">16</span>, <span class="type">const</span> <span class="type">int</span> b_n = <span class="number">32</span>&gt;</span><br><span class="line">__global__ <span class="type">void</span> <span class="built_in">transposeSmempad</span>(<span class="type">float</span> *out, <span class="type">float</span> *in, <span class="type">const</span> <span class="type">int</span> N, <span class="type">const</span> <span class="type">int</span> M)&#123;</span><br><span class="line">    <span class="comment">//block索引</span></span><br><span class="line">    <span class="type">int</span> b_y = blockIdx.y;</span><br><span class="line">    <span class="type">int</span> b_x = blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//block内thread索引</span></span><br><span class="line">    <span class="type">int</span> t_y = threadIdx.y;</span><br><span class="line">    <span class="type">int</span> t_x = threadIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//in, out重定向：</span></span><br><span class="line">    in = &amp;in[<span class="built_in">OFFSET</span>(b_y * b_m, b_x * b_n,N)];</span><br><span class="line">    out = &amp;out[<span class="built_in">OFFSET</span>(b_x * b_n, b_y * b_m,M)];</span><br><span class="line"></span><br><span class="line">    __shared__ <span class="type">float</span> tile[b_m * (b_n + <span class="number">1</span>)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//全局thread索引判定</span></span><br><span class="line">    <span class="type">int</span> g_ty = threadIdx.y + blockDim.y * blockIdx.y;</span><br><span class="line">    <span class="type">int</span> g_tx = threadIdx.x + blockDim.x * blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//block内thread的一维索引</span></span><br><span class="line">    <span class="type">int</span> t_id = <span class="built_in">OFFSET</span>(threadIdx.y, threadIdx.x, b_n);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//保持固定顺序的写入位置</span></span><br><span class="line">    <span class="type">int</span> t_y_out = t_id / b_m;</span><br><span class="line">    <span class="type">int</span> t_x_out = t_id % b_m;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//判定thread有效</span></span><br><span class="line">    <span class="keyword">if</span>(g_ty &lt; M &amp;&amp; g_tx &lt; N)&#123;</span><br><span class="line">        <span class="comment">//写入共享内存位置不变，新增的一列仅为空填充，用于破坏写入和访问的时候的一维索引</span></span><br><span class="line">        tile[<span class="built_in">OFFSET</span>(t_y, t_x, b_n + <span class="number">1</span>)] = in[<span class="built_in">OFFSET</span>(t_y,t_x, N)];</span><br><span class="line">        __syncthreads();</span><br><span class="line"></span><br><span class="line">        out[<span class="built_in">OFFSET</span>(t_y_out, t_x_out, M)] = tile[<span class="built_in">OFFSET</span>(t_x_out, t_y_out, b_n + <span class="number">1</span>)];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260529225005.png"><br><img src="/images/Pasted%20image%2020260529225031.png"></p><h3 id="优化：共享显存和循环展开"><a href="#优化：共享显存和循环展开" class="headerlink" title="优化：共享显存和循环展开"></a>优化：共享显存和循环展开</h3><p><img src="/images/Pasted%20image%2020260529225634.png"></p><p>该核函数通过让每个线程同时处理两个数据元素的方式来提升性能。这种优化策略旨在提高内存访问的并行性，从而更高效地利用设备的内存带宽</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * V4，共享内存 + 填充 + 循环展开</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//我们设计blockDim = (32, 16)</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="type">const</span> <span class="type">int</span> b_m </span>= <span class="number">16</span>, <span class="type">const</span> <span class="type">int</span> b_n = <span class="number">64</span>&gt;</span><br><span class="line">__global__ <span class="type">void</span> <span class="built_in">transposeSmemUnrollPad</span>(<span class="type">float</span> *out, <span class="type">float</span> *in, <span class="type">int</span> N, <span class="type">int</span> M)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">//Dim形状， 一个block，16个warp</span></span><br><span class="line">    <span class="type">int</span> b_y = blockIdx.y;</span><br><span class="line">    <span class="type">int</span> b_x = blockIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> t_y = threadIdx.y;</span><br><span class="line">    <span class="type">int</span> t_x = threadIdx.x;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> t_id = t_y * blockDim.x + t_x;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//因为b_n = 64个float，所以对于一个block，每个thread需要处理的float的个数是2个</span></span><br><span class="line">    <span class="comment">//但是连续32个float可以触发合并访问，所以考虑循环展开，而不是一个thread读取一个float2</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//创建共享内存, 填充最后一列，因为是64列，里面0，32还是会出现bank conflict, </span></span><br><span class="line">    <span class="comment">//所以考虑让一个warp的一个thread同时写，所以也是循环写入</span></span><br><span class="line">    __shared__ <span class="type">float</span> tile[b_m * (b_n + <span class="number">1</span>)];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//这个 thread 负责的第一个输入元素的全局坐标</span></span><br><span class="line">    <span class="type">int</span> g_t_y = t_y + b_y * b_m;</span><br><span class="line">    <span class="type">int</span> g_t_x = t_x + b_x * b_n;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> t_y_out = t_id / blockDim.y;</span><br><span class="line">    <span class="type">int</span> t_x_out = t_id % blockDim.y;</span><br><span class="line"></span><br><span class="line">    in = &amp;in[<span class="built_in">OFFSET</span>(b_y*b_m, b_x * b_n,N)];</span><br><span class="line">    out = &amp;out[<span class="built_in">OFFSET</span>(b_x * b_n, b_y * b_m,M)];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(g_t_y &lt; M &amp;&amp; (g_t_x + blockDim.x) &lt; N)&#123;</span><br><span class="line">        tile[<span class="built_in">OFFSET</span>(t_y, t_x,b_n + <span class="number">1</span>)] = in[<span class="built_in">OFFSET</span>(t_y, t_x,N)];</span><br><span class="line">        tile[<span class="built_in">OFFSET</span>(t_y, t_x + blockDim.x, b_n + <span class="number">1</span>)] = in[<span class="built_in">OFFSET</span>(t_y, t_x + blockDim.x,N)];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __syncthreads();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span>(g_t_y &lt; M &amp;&amp; (g_t_x + blockDim.x) &lt; N)&#123;</span><br><span class="line">        out[<span class="built_in">OFFSET</span>(t_y_out, t_x_out,M)] = tile[<span class="built_in">OFFSET</span>(t_x_out, t_y_out,b_n<span class="number">+1</span>)];</span><br><span class="line">        out[<span class="built_in">OFFSET</span>(t_y_out + blockDim.x, t_x_out, M)] = tile[<span class="built_in">OFFSET</span>(t_x_out,t_y_out + blockDim.x, b_n + <span class="number">1</span>)];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260530120531.png"><br><img src="/images/Pasted%20image%2020260530120542.png" alt="277"></p><p> 但是 unroll 版本里，一个 block 覆盖范围不等于 blockDim</p><p>你现在这个版本：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">blockDim</span> = (<span class="number">32</span>, <span class="number">16</span>)</span><br></pre></td></tr></table></figure><p>但是一个 thread 处理两个 x 方向元素：</p><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-tag">input</span><span class="selector-attr">[row]</span><span class="selector-attr">[tx]</span><span class="selector-tag">input</span><span class="selector-attr">[row]</span><span class="selector-attr">[tx + 32]</span></span><br></pre></td></tr></table></figure><p>所以一个 block 实际覆盖：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">M</span> 方向：<span class="number">16</span> 行N 方向：<span class="number">64</span> 列</span><br></pre></td></tr></table></figure><p>也就是：</p><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">tile_m <span class="operator">=</span> <span class="number">16</span>tile_n <span class="operator">=</span> <span class="number">64</span></span><br></pre></td></tr></table></figure><p>所以 launch 时不能写：</p><figure class="highlight gml"><table><tr><td class="code"><pre><span class="line">grid.<span class="variable language_">x</span> = (N + blockDim.<span class="variable language_">x</span> - <span class="number">1</span>) / blockDim.<span class="variable language_">x</span>;  <span class="comment">// 错，按32列算了</span></span><br></pre></td></tr></table></figure><p>而应该写：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">dim3</span> block(<span class="number">32</span>, <span class="number">16</span>);dim3 grid(    (N + <span class="number">64</span> - <span class="number">1</span>) / <span class="number">64</span>,    (M + <span class="number">16</span> - <span class="number">1</span>) / <span class="number">16</span>);</span><br></pre></td></tr></table></figure><p>或者写得通用一点：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">constexpr</span> int TILE_M = <span class="number">16</span>;constexpr int TILE_N = <span class="number">64</span>;dim3 block(<span class="number">32</span>, <span class="number">16</span>);dim3 grid(    (N + TILE_N - <span class="number">1</span>) / TILE_N,    (M + TILE_M - <span class="number">1</span>) / TILE_M);</span><br></pre></td></tr></table></figure><p>所以重点是：</p><figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line">gridDim 不是一定按 <span class="keyword">blockDim </span>算，而是按“一个 <span class="keyword">block </span>实际覆盖多少数据”来算。</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260530120659.png"></p><p><img src="/images/Pasted%20image%2020260530120735.png" alt="329"></p><p><img src="/images/Pasted%20image%2020260530120825.png" alt="367"><br><img src="/images/Pasted%20image%2020260530120833.png" alt="391"></p><p><img src="/images/Pasted%20image%2020260530120847.png"></p>]]></content>
    
    
    <summary type="html">从 CUDA 线程层级、内存模型与同步机制出发，记录 softmax、reduce、matmul 等基础算子的实现与优化。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="算子与性能" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E7%AE%97%E5%AD%90%E4%B8%8E%E6%80%A7%E8%83%BD/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="Kernel" scheme="https://liangji-seu.github.io/tags/Kernel/"/>
    
    <category term="GPU" scheme="https://liangji-seu.github.io/tags/GPU/"/>
    
  </entry>
  
  <entry>
    <title>infra nano-vllm 源码解读</title>
    <link href="https://liangji-seu.github.io/2026/06/14/infra-nano-vllm%20%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/"/>
    <id>https://liangji-seu.github.io/2026/06/14/infra-nano-vllm%20%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/</id>
    <published>2026-06-14T15:51:25.000Z</published>
    <updated>2026-10-03T10:34:31.623Z</updated>
    
    <content type="html"><![CDATA[<p>本小节，主要来通过nano-vllm的源码解读，来了解推理系统vllm的整个架构设计，推理优化的点，并最终实现手写nano-vllm。</p><h1 id="项目架构"><a href="#项目架构" class="headerlink" title="项目架构"></a>项目架构</h1><p>这里先分析一下整个nano-vllm的项目架构</p><p><code>nano-vllm</code> 是一个教学级别的轻量化 vLLM<strong>推理引擎</strong>实现</p><p>整体框架图如下：<br><img src="/images/Pasted%20image%2020260516183919.png"><img src="/images/Pasted%20image%2020260516184129.png"></p><p>所以，总共包含4个部分：</p><ul><li>API层，提供用户接口，比如对话等</li><li>引擎层：提供调度，内存管理</li><li>模型层：提供算子+模型定义</li><li>工具层</li></ul><h2 id="api层"><a href="#api层" class="headerlink" title="api层"></a>api层</h2><p>这一层主要就是面向用户的接口封装：</p><p>用户只需 <code>LLM(model_path, ...)</code> + <code>llm.generate(prompts, sampling_params)</code> 两行即可完成推理</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">├── config.py                 <span class="comment"># ⚙️ 配置管理</span></span><br><span class="line">├── llm.py                   <span class="comment">#   用户接口入口</span></span><br><span class="line">└── sampling_params.py       <span class="comment">#   采样参数定义</span></span><br></pre></td></tr></table></figure><h2 id="引擎层"><a href="#引擎层" class="headerlink" title="引擎层"></a>引擎层</h2><p>这是vllm的核心</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">├── engine/                    <span class="comment">#   推理引擎核心</span></span><br><span class="line">│   ├── llm_engine.py         <span class="comment">#   └── 总协调器，驱动整个推理流程</span></span><br><span class="line">│   ├── scheduler.py          <span class="comment">#   └── 智能调度器，决定执行顺序</span></span><br><span class="line">│   ├── block_manager.py      <span class="comment">#   └── KV缓存内存管理 (PagedAttention核心)</span></span><br><span class="line">│   ├── model_runner.py       <span class="comment">#   └── 单GPU上的模型执行器</span></span><br><span class="line">│   └── sequence.py           <span class="comment">#   └── 请求序列的数据结构</span></span><br></pre></td></tr></table></figure><h2 id="模型算子层"><a href="#模型算子层" class="headerlink" title="模型算子层"></a>模型算子层</h2><p>这一层主要提供推理框架</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">├── layers/                    # ⚙️ 神经网络层实现</span><br><span class="line">│   ├── attention.py          #   └── FlashAttention + KV缓存管理</span><br><span class="line">│   ├── sampler.py            #   └── 从logits采样生成token</span><br><span class="line">│   ├── linear.py             #   └── 支持张量并行的线性层</span><br><span class="line">│   ├── layernorm.py          #   └── RMS LayerNorm</span><br><span class="line">│   ├── rotary_embedding.py   #   └── 旋转位置编码 (RoPE)</span><br><span class="line">│   ├── activation.py         #   └── 激活函数 (SiLU)</span><br><span class="line">│   └── embed_head.py         #   └── 词嵌入和语言模型头</span><br><span class="line">├── models/                    #  ️ 具体模型架构</span><br><span class="line">│   └── qwen3.py              #   └── Qwen3模型完整实现</span><br></pre></td></tr></table></figure><p>注意，这里我们的nano-vllm，并没有完全脱离pytorch，而是在torch的基础上，做了定制和优化。</p><p><img src="/images/Pasted%20image%2020260516185301.png"></p><p>各层的实现不同<br><img src="/images/Pasted%20image%2020260516185311.png"></p><p>相当于就是对大模型的各个主要组件（算子），为了解决他们的性能问题，而进行了重构</p><p><img src="/images/Pasted%20image%2020260516185412.png"><br>主要有：</p><ul><li>FlashAttention, 解决注意力计算的O(n2)的显存问题</li><li>FFN，解决张量并行问题（TP）</li><li>Triton KVCache kernel, 解决高效写入KV Cache的问题</li><li>融合算子</li></ul><p>所以本质上，nano-vllm是重写的模型推理涉及的6个特定nn库里面的层：</p><figure class="highlight vim"><table><tr><td class="code"><pre><span class="line">PyTorch 提供的                   nano-vllm 复写的</span><br><span class="line">───────────────                 ─────────────────</span><br><span class="line"><span class="keyword">nn</span>.Linear          ──→   自定义 Linear（加 TP 权重切分）</span><br><span class="line"><span class="keyword">nn</span>.Embedding       ──→   自定义 Embedding（加 Vocab Parallel）</span><br><span class="line"><span class="keyword">nn</span>.LayerNorm       ──→   自定义 RMSNorm（加 residual fused）</span><br><span class="line"><span class="keyword">nn</span>.MultiheadAttention ─→ 自定义 Attention（FlashAttention + KV Cache）</span><br><span class="line">无                  ──→   Sampler（Gumbel-<span class="built_in">max</span> 采样）</span><br><span class="line">无                  ──→   RotaryEmbedding（RoPE 位置编码）</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260516185921.png"></p><h2 id="工具层"><a href="#工具层" class="headerlink" title="工具层"></a>工具层</h2><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">├── utils/                     #   工具模块</span><br><span class="line">│   ├── context.py            #   └── 全局上下文状态管理</span><br><span class="line">│   └── loader.py             #   └── 模型权重加载器</span><br></pre></td></tr></table></figure><h1 id="数据处理流程"><a href="#数据处理流程" class="headerlink" title="数据处理流程"></a>数据处理流程</h1><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">用户输入 → Tokenizer → Sequence → Scheduler → ModelRunner → Model → Sampler → 输出</span><br><span class="line"></span><br><span class="line">详细展开：</span><br><span class="line">prompts     token_ids    Sequence     scheduled    input_ids    logits   token_ids    decoded_text</span><br><span class="line">  ↓            ↓           ↓           seqs          ↓           ↓          ↓            ↓</span><br><span class="line">&quot;Hello&quot;  →  [123,45]  →  Seq#1    →  [Seq#1]   →  tensor   →  tensor  →    67      →  &quot; world&quot;</span><br><span class="line">&quot;Hi&quot;     →  [89,12]   →  Seq#2    →  [Seq#2]   →  [...]    →  [...]   →    23      →  &quot; there&quot;</span><br></pre></td></tr></table></figure><hr><h1 id="python语法补充"><a href="#python语法补充" class="headerlink" title="python语法补充"></a>python语法补充</h1><p><img src="/images/Pasted%20image%2020260517134106.png" alt="379"></p><hr><h2 id="类型注解"><a href="#类型注解" class="headerlink" title="类型注解"></a>类型注解</h2><p>简单来说，这是在告诉 Python（以及阅读代码的人）：<strong>“这个变量应该是某种特定的类型”。</strong></p><p>Python 本身是动态类型语言（变量可以随时改变类型），但类型注解能让代码更易读、减少 Bug，并且让 IDE（如 VS Code&#x2F;PyCharm）提供更精准的代码补全</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">model : <span class="built_in">str</span></span><br></pre></td></tr></table></figure><ul><li><strong>语法：</strong> <code>变量名: 类型</code></li><li><strong>理解：</strong> 声明 <code>model</code> 变量应该是一个<strong>字符串（string）</strong>。这只是个标注，不会强制阻止你给它赋值数字，但静态检查工具（如 MyPy）会报错。</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">max_tokens: <span class="built_in">int</span> = <span class="number">64</span></span><br></pre></td></tr></table></figure><ul><li><strong>语法：</strong> <code>变量名: 类型 = 默认值</code></li><li><strong>理解：</strong> 声明 <code>max_tokens</code> 是<strong>整数（integer）</strong>，并且它的初始值是 <code>64</code>。</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">prompts: <span class="built_in">list</span>[<span class="built_in">str</span>] | <span class="built_in">list</span>[<span class="built_in">list</span>[<span class="built_in">int</span>]]</span><br></pre></td></tr></table></figure><ul><li><strong>语法：</strong> <code>A | B</code> (Python 3.10+ 的新写法)</li><li><strong>理解：</strong> <code>|</code> 符号读作 <strong>“或者”</strong>。<ul><li>它可以是一个字符串列表：<code>[&quot;hello&quot;, &quot;world&quot;]</code></li><li><strong>或者</strong>是一个嵌套的整数列表：<code>[[1, 2], [3, 4]]</code></li></ul></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">hf_config: AutoConfig | <span class="literal">None</span></span><br></pre></td></tr></table></figure><ul><li><strong>语法：</strong> <code>类型 | None</code></li><li><strong>理解：</strong> 表示这个变量<strong>要么</strong>是一个 <code>AutoConfig</code> 对象，<strong>要么</strong>是 <code>None</code>（空）。</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">segs: <span class="built_in">list</span>[<span class="type">Sequence</span>]</span><br></pre></td></tr></table></figure><ul><li><strong>语法：</strong> <code>容器类型[内部元素类型]</code></li><li><strong>理解：</strong> 这是一个<strong>列表</strong>，但列表里的每一个元素都必须符合 <code>Sequence</code>（序列）协议（比如列表、元组或字符串）。</li></ul><h2 id="python一切皆对象"><a href="#python一切皆对象" class="headerlink" title="python一切皆对象"></a>python一切皆对象</h2><ul><li>整数 <code>1</code> 是对象</li><li>字符串 <code>&quot;hello&quot;</code> 是对象</li><li>列表 <code>[]</code> 是对象</li><li>函数 <code>def func():</code> 是对象</li><li>**类 <code>MyClass</code> 是 <strong><code>type</code> 类的实例</strong>。</li></ul><p>所以，定义一个class Book,他有很多属性</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Book</span>:</span><br><span class="line">xxx</span><br><span class="line"></span><br><span class="line">Book.xxx 这里面有</span><br><span class="line">__annotations__, 是一个字典（哈希表），里面存放的是类/方法里面的所有类型注解</span><br><span class="line">author: <span class="built_in">str</span>, 这种属于类的类型注解</span><br><span class="line">__base__,    该类的父类（类型，默认是<span class="built_in">object</span>类&lt;<span class="keyword">class</span> <span class="string">&#x27;object&#x27;</span>&gt;）</span><br><span class="line">Book 类，**直接父类（继承的类）是 `<span class="built_in">object</span>`**，所以 `Book.__base__` 是 `<span class="built_in">object</span>`</span><br><span class="line">__bases__,   该类的所有父类集合（元组）</span><br><span class="line">__basicsize__, 类对象的内存占用字节数</span><br><span class="line">__call__,    方法, 类对象的仿函数</span><br><span class="line">__class__,    </span><br><span class="line">Book 类，是 `<span class="built_in">type</span>` 这个元类的实例（对象）**，所以 `Book.__class__` 是 `<span class="built_in">type</span>`       __delattr__, 方法, 删除对象属性时会调用的方法（比如 `<span class="keyword">del</span> obj.title`）</span><br><span class="line">__dict__,  字典</span><br><span class="line">类的命名空间，存所有类属性、方法（包括你写的 `title`/`author`，以及自动生成的特殊方法）</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260517112701.png" alt="476"></p><p>所以，一个类，默认有三个方法：</p><ul><li><strong>init</strong></li><li>call</li><li>delattr</li></ul><p><strong>当然这不是全部，一个类实例，有很多个方法</strong><br><img src="/images/Pasted%20image%2020260517112943.png" alt="414"><br><img src="/images/Pasted%20image%2020260517113006.png" alt="465"></p><p><strong>因为 Python 的 <code>object</code> 类默认提供了这些方法的基础实现。</strong></p><p><img src="/images/Pasted%20image%2020260517113108.png" alt="256"></p><h3 id="常用的类实例的方法"><a href="#常用的类实例的方法" class="headerlink" title="常用的类实例的方法"></a>常用的类实例的方法</h3><p><strong><code>__repr__</code></strong>：重写它，让 <code>print(obj)</code> 输出好看的信息，而不是 <code>&lt;Book object at 0x...&gt;</code>。</p><p><strong><code>__eq__</code> &#x2F; <code>__hash__</code></strong>：如果想让对象可以用 &#x3D;&#x3D; 比较，相当于重载</p><p><strong><code>__getattr__</code> &#x2F; <code>__setattr__</code></strong>：需要自定义属性访问逻辑时（比如做代理、动态属性）</p><p><strong><code>__call__</code></strong>：想让对象可以像函数一样被调用时（比如 <code>obj()</code>）</p><hr><h2 id="dataclass装饰器"><a href="#dataclass装饰器" class="headerlink" title="dataclass装饰器"></a>dataclass装饰器</h2><p><strong><mark style="background:#affad1">装饰器</mark></strong><br>就是一个自动插件，你写好一个基础的类，加上这个帽子，就可以在后台自动为你的基础类添加很多功能。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@dataclass(<span class="params">slots = <span class="literal">True</span></span>)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">mybase</span>:</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以装饰器的作用，就是帮你默认改造上面<strong>类实例的一些默认方法</strong></p><h3 id="dataclass改造的方法"><a href="#dataclass改造的方法" class="headerlink" title="dataclass改造的方法"></a>dataclass改造的方法</h3><p><mark style="background:#affad1">dataclass</mark><br><code>dataclass</code> 是 Python 3.7 引入的一个模块，专门用来创建<strong>只负责保存数据</strong>的类。</p><p>改造了哪些方法呢？</p><ol><li><strong>__init__方法</strong></li></ol><p>自动识别你的类型注解，然后写入你的init来帮你构造初始化函数</p><p><img src="/images/Pasted%20image%2020260517113537.png" alt="246"></p><ol start="2"><li>**__repr__方法</li></ol><p><strong>格式化打印方法</strong>， 没有写的话，打印对象，默认会打印地址，相当于重载了&lt;&lt;</p><ol start="3"><li><p><strong><strong>eq</strong></strong><br>重载&#x3D;&#x3D;</p></li><li><p><mark style="background:#fff88f">如果开启了order&#x3D;True参数</mark><img src="/images/Pasted%20image%2020260517113958.png"></p></li><li><p><code>frozen=True</code> → 让对象不可修改<img src="/images/Pasted%20image%2020260517114049.png"></p></li><li><p><code>slots=True</code><img src="/images/Pasted%20image%2020260517114236.png"><br>正常，我们是通过__dict__字典，来查看所有的成员变量的，也可以动态增减成员属性。但是占内存，速度慢</p></li></ol><p><mark style="background:#fff88f">而开启了slots之后，就等于锁死了属性，不能动态增减属性，但是内存占用少了，访问速度更快，但是没有__dict__了。</mark></p><p><img src="/images/Pasted%20image%2020260517114445.png" alt="300"><img src="/images/Pasted%20image%2020260517114509.png" alt="363"></p><hr><h2 id="属性装饰器"><a href="#属性装饰器" class="headerlink" title="属性装饰器"></a>属性装饰器</h2><p><code>@property</code> 是一个装饰器，能把类里的<strong>方法伪装成属性</strong>，让你可以像访问普通变量一样调用它，但底层执行的是函数逻辑。</p><h3 id="三种基本用法"><a href="#三种基本用法" class="headerlink" title="三种基本用法"></a>三种基本用法</h3><ol><li><strong>只读属性</strong></li></ol><p>某个值是计算出来的，不想让用户直接修改<br><img src="/images/Pasted%20image%2020260517115122.png" alt="379"></p><p>这样可以实现一个只读属性</p><ol start="2"><li><strong>属性写校验</strong></li></ol><p>我们在修改某个属性时，想要先校验，因为直接修改属性肯定无法校验，所以需要用<br>@[属性].setter</p><p><strong>来设置这个属性的写入方法</strong></p><p>注意：<code>@name.setter</code> 是 <code>property</code> 的语法糖，它必须跟在 <code>@property</code> 装饰的 <code>name</code> 方法后面才能用。所以必须依赖@property才行</p><ol start="3"><li><strong>可删除属性</strong></li></ol><p><code>@xxx.deleter</code>，不常用<br>场景：删除属性时需要做清理工作。</p><h3 id="语法糖"><a href="#语法糖" class="headerlink" title="语法糖"></a>语法糖</h3><p>语法糖（Syntactic Sugar），就是「让代码写起来更甜、更舒服，但功能上没有任何变化的语法」。</p><p>它本质上是语言给你提供的「便捷写法」，底层会自动帮你转换成等价的、更繁琐的代码，让你少写重复、难看的代码。<br><img src="/images/Pasted%20image%2020260517120306.png" alt="480"></p><hr><h2 id="协议（Protocol）"><a href="#协议（Protocol）" class="headerlink" title="协议（Protocol）"></a>协议（Protocol）</h2><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># sequence.py</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Sequence</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__len__</span>(<span class="params">self</span>):           <span class="comment"># 实现后可以用 len(seq)</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.num_tokens</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getitem__</span>(<span class="params">self, key</span>):  <span class="comment"># 实现后可以用 seq[3] 取值</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.token_ids[key]</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getstate__</span>(<span class="params">self</span>):      <span class="comment"># 控制 pickle 序列化行为</span></span><br><span class="line">        ...</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__setstate__</span>(<span class="params">self, state</span>):  <span class="comment"># 控制 pickle 反序列化行为</span></span><br><span class="line">        ...</span><br></pre></td></tr></table></figure><h3 id="Python-数据模型（Data-Model）（魔术方法-双下方法）"><a href="#Python-数据模型（Data-Model）（魔术方法-双下方法）" class="headerlink" title="Python 数据模型（Data Model）（魔术方法&#x2F;双下方法）"></a>Python 数据模型（Data Model）（魔术方法&#x2F;双下方法）</h3><p>这一块属于python 协议 编程的核心：<strong>只要你在类里实现了特定的魔术方法，你的对象就可以表现得像内置类型（如列表、字典、数字）一样</strong>。</p><p><mark style="background:#affad1">容器与序列协议 (Container&#x2F;Sequence Protocol)</mark></p><p>类里面实现的__len__, <strong>getitem</strong> 这些方法，能够把我自己的类伪装成一个列表&#x2F;元组</p><ul><li><p><strong><code>__len__(self)</code></strong>:</p><ul><li><strong>触发方式：</strong> 调用 <code>len(obj)</code>。</li><li><strong>作用：</strong> 返回对象的长度。</li></ul></li><li><p><strong><code>__getitem__(self, key)</code></strong>: <strong>数组下标访问</strong></p><ul><li><strong>触发方式：</strong> 使用索引取值 <code>obj[key]</code>。</li><li><strong>作用：</strong> 定义如何获取数据。支持 <code>key</code> 为整数（索引）或 <code>slice</code> 对象（切片 <code>obj[1:3]</code>）。</li></ul></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 注意这个方法只能返回整形int</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__len__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="number">10</span></span><br><span class="line">        </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getitem__</span>(<span class="params">self, key</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;i find key value&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">book = Book_new(<span class="string">&quot;test&quot;</span>,<span class="string">&quot;liangji&quot;</span>, <span class="number">1000</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(book))</span><br><span class="line"><span class="built_in">print</span>(book[<span class="number">1</span>])</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">生命周期与初始化 (Lifecycle)</mark></p><ul><li><strong><code>__init__(self, ...)</code></strong>: 初始化对象。</li><li><strong><code>__new__(self, ...)</code></strong>: 真正创建实例的方法（在 <code>__init__</code> 之前执行，常用于单例模式）。</li><li><strong><code>__del__(self)</code></strong>: 析构方法，当对象被垃圾回收时触发</li></ul><p><mark style="background:#affad1">序列化协议 (Serialization)</mark></p><ul><li><p><strong><code>__getstate__(self)</code></strong>:</p><ul><li><strong>作用：</strong> 当你尝试“打包”（序列化）一个对象存入磁盘或在网络传输时，这个方法决定哪些数据被保存。</li><li><strong>场景：</strong> 如果类里有一个巨大的临时缓存或无法序列化的文件句柄，你可以通过这个方法把它们排除在外。</li></ul></li><li><p><strong><code>__setstate__(self, state)</code></strong>:</p><ul><li><strong>作用：</strong> “拆包”（反序列化）时，如何根据保存的数据重建对象。</li></ul></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> pickle</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyClass</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, name, file_handle</span>):</span><br><span class="line">        <span class="variable language_">self</span>.name = name</span><br><span class="line">        <span class="variable language_">self</span>.file_handle = file_handle</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getstate__</span>(<span class="params">self</span>):</span><br><span class="line">        state = <span class="variable language_">self</span>.__dict__.copy()</span><br><span class="line">        <span class="keyword">del</span> state[<span class="string">&#x27;file_handle&#x27;</span>]</span><br><span class="line">        <span class="keyword">return</span> state</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__setstate__</span>(<span class="params">self, state</span>):</span><br><span class="line">        <span class="variable language_">self</span>.__dict__.update(state)</span><br><span class="line">        <span class="variable language_">self</span>.file_handle = <span class="built_in">open</span>(<span class="string">f&quot;<span class="subst">&#123;self.name&#125;</span>.txt&quot;</span>, <span class="string">&quot;w&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    obj = MyClass(<span class="string">&quot;demo&quot;</span>, <span class="built_in">open</span>(<span class="string">&quot;demo.txt&quot;</span>, <span class="string">&quot;w&quot;</span>))</span><br><span class="line"></span><br><span class="line">    serialized_data = pickle.dumps(obj)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;ok&quot;</span>)</span><br><span class="line"></span><br><span class="line">    restored_obj = pickle.loads(serialized_data)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;ok&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;name: <span class="subst">&#123;restored_obj.name&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;file_handle 是否重建: <span class="subst">&#123;restored_obj.file_handle <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    restored_obj.file_handle.close()</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260517140210.png"></p><p>其实说白了，就是特地指定哪些能打包序列化，哪些不用打包序列化</p><p><mark style="background:#affad1">对象表示 (Object Representation)</mark></p><p>决定对象在不同环境下如何显示。</p><ul><li><strong><code>__str__</code></strong>: <code>print(obj)</code> 时显示的对用户友好的字符串。</li><li><strong><code>__repr__</code></strong>: 开发调试时显示的字符串（应尽量能通过 <code>eval(repr(obj))</code> 还原对象）。</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, name, age</span>):</span><br><span class="line">        <span class="variable language_">self</span>.name = name</span><br><span class="line">        <span class="variable language_">self</span>.age = age</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 给用户看的友好信息</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__str__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;Person对象：名字是 <span class="subst">&#123;self.name&#125;</span>，年龄是 <span class="subst">&#123;self.age&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 给开发者调试用的，最好能通过eval还原对象</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__repr__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;Person(&#x27;<span class="subst">&#123;self.name&#125;</span>&#x27;, <span class="subst">&#123;self.age&#125;</span>)&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试</span></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    p = Person(<span class="string">&quot;小明&quot;</span>, <span class="number">18</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;=== 直接print(obj) 会调用 __str__ ===&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(p)  <span class="comment"># 输出：Person对象：名字是 小明，年龄是 18</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n=== 交互环境/调试/repr() 会调用 __repr__ ===&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="built_in">repr</span>(p))  <span class="comment"># 输出：Person(&#x27;小明&#x27;, 18)</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n=== eval(repr(obj)) 可以还原对象 ===&quot;</span>)</span><br><span class="line">    p2 = <span class="built_in">eval</span>(<span class="built_in">repr</span>(p))</span><br><span class="line">    <span class="built_in">print</span>(p2.name, p2.age)  <span class="comment"># 输出：小明 18</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260517140646.png"></p><h2 id="枚举类"><a href="#枚举类" class="headerlink" title="枚举类"></a>枚举类</h2><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> enum <span class="keyword">import</span> Enum, auto</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">status</span>(<span class="title class_ inherited__">Enum</span>):</span><br><span class="line">    STATUS_A = auto()  <span class="comment"># 自动分配1</span></span><br><span class="line">    STATUS_B = auto()  <span class="comment"># 自动分配2</span></span><br><span class="line">    STATUS_C = auto()  <span class="comment"># 自动分配3</span></span><br><span class="line"></span><br><span class="line">current_status = status.STATUS_A</span><br><span class="line"><span class="built_in">print</span>(status.STATUS_A)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260517141036.png" alt="599"></p><p>Enum是枚举类的基类，auto是一个函数估计，提供自动计数，防止重复</p><h2 id="另外的装饰器"><a href="#另外的装饰器" class="headerlink" title="另外的装饰器"></a>另外的装饰器</h2><p><strong>装饰器的本质是一个“包装器”，在不修改原函数内部代码的前提下，为函数增加额外的功能。</strong></p><h3 id="pytorch装饰器"><a href="#pytorch装饰器" class="headerlink" title="pytorch装饰器"></a>pytorch装饰器</h3><p>在sampler.py， model_runner.py这种速度要求高的文件里面，有以下两种装饰器</p><h4 id="torch-compile-内核编译优化"><a href="#torch-compile-内核编译优化" class="headerlink" title="@torch.compile (内核编译优化)"></a>@torch.compile (内核编译优化)</h4><ul><li><strong>语法点：</strong> 这是 PyTorch 2.0+ 的核心特性。</li><li><strong>背后的原理：</strong> 默认情况下，Python 是“一行一行”执行代码的（解释型）。这个装饰器会把 Python 函数转换成高度优化的<strong>中间表示 (IR)</strong>，然后针对 GPU 生成专门的内核代码（Triton 内核）。</li><li><strong>效果：</strong> 极大地减少 CPU 启动开销（Overhead），提高 GPU 的吞吐量。它让你的 Python 代码跑起来接近 C++ 的速度。</li></ul><h4 id="torch-inference-mode-上下文管理"><a href="#torch-inference-mode-上下文管理" class="headerlink" title="@torch.inference_mode() (上下文管理)"></a>@torch.inference_mode() (上下文管理)</h4><ul><li><strong>语法点：</strong> 这是一个<strong>带括号</strong>的装饰器（实际上是一个装饰器工厂）。</li><li><strong>背后的原理：</strong> 它告诉 PyTorch：“我现在的操作只是为了推理（预测），不是为了训练。”</li><li><strong>效果：</strong><ul><li><strong>关闭梯度计算</strong>（不记录反向传播所需的中间变量）。</li><li><strong>减少显存消耗</strong>。</li><li><strong>性能更快</strong>（比旧的 <code>with torch.no_grad():</code> 甚至还要更快一些）。</li></ul></li></ul><h3 id="functools工具类装饰器"><a href="#functools工具类装饰器" class="headerlink" title="functools工具类装饰器"></a>functools工具类装饰器</h3><h4 id="lru-cache-maxsize-1-最近最少使用缓存"><a href="#lru-cache-maxsize-1-最近最少使用缓存" class="headerlink" title="@lru_cache(maxsize&#x3D;1) (最近最少使用缓存)"></a>@lru_cache(maxsize&#x3D;1) (最近最少使用缓存)</h4><ul><li><strong>语法点：</strong> <code>lru_cache</code> 来自标准库 <code>functools</code>。</li><li><strong>背后的原理：</strong> 它给函数增加了一个“备忘录”。当你第一次用参数 <code>A</code> 调用 <code>get_rope</code> 时，它计算结果并存起来；第二次用同样的参数调用时，它直接从内存里拿结果，不再执行函数内部逻辑。</li><li><strong>场景：</strong> 在大模型中，<code>get_rope</code>（获取旋转位置嵌入）通常涉及复杂的三角函数计算。因为模型的旋转位置编码在运行中通常是固定的，所以用 <code>maxsize=1</code> 缓存住最近一次的结果，可以省去每一层都重复计算的开销。</li></ul><h2 id="装饰器的基本知识"><a href="#装饰器的基本知识" class="headerlink" title="装饰器的基本知识"></a>装饰器的基本知识</h2><p>要掌握这一块语法，你需要理解以下三个层级：</p><table><thead><tr><th><strong>层级</strong></th><th><strong>表现形式</strong></th><th><strong>核心作用</strong></th></tr></thead><tbody><tr><td><strong>基础层</strong></td><td><code>def decorator(func):</code></td><td>理解装饰器本质是一个接收函数并返回函数的高阶函数。</td></tr><tr><td><strong>进阶层</strong></td><td><code>@decorator(args)</code></td><td>理解带参数的装饰器（如 <code>lru_cache(1)</code>），它先执行函数生成真正的装饰器。</td></tr><tr><td><strong>工程层</strong></td><td><code>@torch.compile</code> 等</td><td>掌握在特定框架中，如何利用装饰器<strong>无侵入</strong>地实现<strong>性能优化</strong>。</td></tr></tbody></table><h2 id="基础数据结构"><a href="#基础数据结构" class="headerlink" title="基础数据结构"></a>基础数据结构</h2><h3 id="双端队列deque"><a href="#双端队列deque" class="headerlink" title="双端队列deque"></a>双端队列deque</h3><p><strong>就是双向链表</strong>，数据可以从两头进，也可以从两头出。</p><p>因为是双向链表，所以，增，删首尾节点的时间复杂度是O（1），对比list，增加删除最后一个元素是O（1），但是增删第一个元素是O（n）</p><ol><li><strong>添加元素（push）</strong><ol><li>.append(x): 尾部插入</li><li>.appendleft(x):队首插入（高优先级立刻处理）</li></ol></li><li><strong>弹出元素（pop）</strong>、<ol><li>pop(): 从队尾取出一个并删除</li><li>popleft(): 从队首取出一个并删除（调度器常用，获取下一个待处理的任务）</li></ol></li><li><strong>批量操作(Extend)</strong><ol><li>extend(iterable): 在右侧批量添加</li><li>extendleft(iterable): 在左侧逐个批量添加（最终方向相反）</li></ol></li><li><strong>特殊功能（限制长度）</strong><ol><li>d &#x3D; deque(maxlen&#x3D;10)<ol><li>满了之后，右边加一个，左边会被挤掉一个，用于处理<strong>滑动窗口</strong>或者<strong>最近日志记录</strong></li></ol></li></ol></li></ol><p><img src="/images/Pasted%20image%2020260517142819.png"></p><p>deque的初始化，</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> collections <span class="keyword">import</span> deque</span><br><span class="line"></span><br><span class="line">dq = deque([<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>])</span><br></pre></td></tr></table></figure><p>元素可以是任意类型，只要是可迭代对象，就能进行初始化</p><p><mark style="background:#affad1">判断一个对象是否是可迭代的</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> collections.abc <span class="keyword">import</span> Iterable </span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>([<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>], Iterable)) <span class="comment"># True </span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>(<span class="string">&quot;abc&quot;</span>, Iterable)) <span class="comment"># True </span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>(<span class="number">123</span>, Iterable)) <span class="comment"># False</span></span><br></pre></td></tr></table></figure><h4 id="isinstance-类型检查"><a href="#isinstance-类型检查" class="headerlink" title="isinstance 类型检查"></a>isinstance 类型检查</h4><p>其中<code>isinstance</code> 是 Python <strong>最常用、最重要</strong>的内置函数之一，专门用来<strong>判断一个对象 是不是 某种类型</strong>。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="built_in">isinstance</span>(要检查的对象, 类型)</span><br></pre></td></tr></table></figure><p>返回结果：<strong>True &#x2F; False</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 判断 100 是不是 int 类型 </span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>(<span class="number">100</span>, <span class="built_in">int</span>)) <span class="comment"># True </span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 判断 &quot;abc&quot; 是不是 str 类型 </span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>(<span class="string">&quot;abc&quot;</span>, <span class="built_in">str</span>)) <span class="comment"># True </span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 判断 [1,2] 是不是 list 类型 </span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">isinstance</span>([<span class="number">1</span>,<span class="number">2</span>], <span class="built_in">list</span>)) <span class="comment"># True</span></span><br></pre></td></tr></table></figure><h3 id="itertools-count类"><a href="#itertools-count类" class="headerlink" title="itertools.count类"></a>itertools.count类</h3><p><code>itertools.count()</code>, 为了给每一个<strong>新生成的对象</strong>（比如一条 Sequence&#x2F;请求）分配一个<strong>全局唯一的 ID</strong></p><p>它是 Python 内置标准库中的一个<strong>无限迭代器</strong>。</p><ul><li><strong>形象理解</strong>：它就像一个永不停歇的计数器。你不需要告诉它什么时候停止，只要你找它要（调用 <code>next()</code>），它就会给你下一个数字。</li><li><strong>默认行为</strong>：如果不传参数，它从 <code>0</code> 开始，步长为 <code>1</code>。即：<code>0, 1, 2, 3...</code></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> itertools <span class="keyword">import</span> count</span><br><span class="line"></span><br><span class="line"><span class="comment"># 1. 创建计数器对象</span></span><br><span class="line">counter = count(start=<span class="number">10</span>, step=<span class="number">2</span>)  <span class="comment"># 也可以自定义起点和步长</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 获取下一个值</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">next</span>(counter)) <span class="comment"># 10</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">next</span>(counter)) <span class="comment"># 12</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">next</span>(counter)) <span class="comment"># 14</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">next</span>(counter)) <span class="comment"># 16</span></span><br></pre></td></tr></table></figure><h3 id="列表推导式"><a href="#列表推导式" class="headerlink" title="列表推导式"></a>列表推导式</h3><p><code>outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())]</code></p><ul><li><strong>外层 <code>[]</code></strong>：表示最终结果是一个列表。</li><li><strong><code>outputs[seq_id]</code></strong>：这是<strong>结果表达式</strong>（你想存进列表里的东西）。</li><li><strong><code>for seq_id in sorted(outputs.keys())</code></strong>：这是<strong>迭代逻辑</strong>。它先获取字典所有的键，排序，然后逐个取出。</li></ul><h3 id="字典推导式"><a href="#字典推导式" class="headerlink" title="字典推导式"></a>字典推导式</h3><p><code>config_kwargs = {k: v for k, v in kwargs.items() if k in config_fields}</code></p><p>其中：<code>items()</code> 是<strong>字典</strong>（dict）的内置方法，<strong>把字典里的所有「键值对」一次性取出来</strong>。</p><blockquote><p>item() 这个是单数，他的作用是把只有一个元素的数组&#x2F;列表，转换成一个值</p></blockquote><h3 id="集合推导式"><a href="#集合推导式" class="headerlink" title="集合推导式"></a>集合推导式</h3><p>就是返回的是set集合<br><code>my_set = {x for x in [1, 2, 2, 3, 4, 4, 5] if x &gt; 2}</code></p><h3 id="enumerate"><a href="#enumerate" class="headerlink" title="enumerate"></a>enumerate</h3><p><code>enumerate</code> 是 Python 里一个<strong>超常用、超好用</strong>的内置函数，专门用来「遍历序列时，<strong>同时拿到索引和元素</strong>」</p><p><code>enumerate</code> 就是给你要遍历的东西，<strong>自动加上序号</strong>，让你不用自己写 <code>i = 0; i += 1</code> 来计数。</p><h3 id="常用数据结构对应表"><a href="#常用数据结构对应表" class="headerlink" title="常用数据结构对应表"></a>常用数据结构对应表</h3><ol><li><strong>list 列表</strong> &#x3D; C++ <code>vector</code> 动态数组</li><li><strong>tuple 元组</strong> &#x3D; 只读不可变数组</li><li><strong>dict 字典</strong> &#x3D; 哈希表 <code>unordered_map</code></li><li><strong>deque 双向队列</strong> &#x3D; 双向链表</li><li><strong>set 集合</strong> &#x3D; 哈希集合 <code>unordered_set</code><ul><li>自动去重、判断成员超快</li></ul></li></ol><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">s</span> = &#123;<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>&#125;</span><br></pre></td></tr></table></figure><ol start="6"><li><strong>frozenset 不可变集合</strong><ul><li>不能增删，可当字典 key</li></ul></li><li><strong>heapq 小顶堆</strong> &#x3D; 优先队列 <code>priority_queue</code><ul><li>用来做 TopK、排序、任务优先级</li></ul></li><li><strong>collections.defaultdict</strong><ul><li>带默认值字典，避免键不存在报错</li></ul></li><li><strong>collections.OrderedDict</strong><ul><li>有序字典（3.7+dict 本身也有序）</li></ul></li><li><strong>collections.Counter</strong><ul><li>计数统计神器，统计列表 &#x2F; 字符串元素频次</li></ul></li><li><strong>collections.namedtuple</strong><ul><li>命名元组，轻量级结构体</li></ul></li><li><strong>array.array</strong><ul><li>纯同类型紧凑数组，比 list 省内存</li></ul></li><li><strong>bisect 有序数组</strong><ul><li>维持列表有序，二分查找插入</li></ul></li></ol><h4 id="最简用途速记"><a href="#最简用途速记" class="headerlink" title="最简用途速记"></a>最简用途速记</h4><ul><li>存有序可改数据 → <strong>list</strong></li><li>固定不变数据 → <strong>tuple</strong></li><li>键值查找 → <strong>dict</strong></li><li>头尾快速增删 → <strong>deque</strong></li><li>去重、判存在 → <strong>set</strong></li><li>排序优先取最值 → <strong>heapq</strong></li><li>统计次数 → <strong>Counter</strong></li><li>轻量结构体 → <strong>namedtuple</strong></li></ul><h2 id="序列化pickle"><a href="#序列化pickle" class="headerlink" title="序列化pickle"></a>序列化pickle</h2><p><strong>pickle 序列化</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># model_runner.py</span></span><br><span class="line"><span class="keyword">import</span> pickle</span><br><span class="line">data = pickle.dumps([method_name, *args])   <span class="comment"># 对象 → 字节</span></span><br><span class="line">method_name, *args = pickle.loads(data)      <span class="comment"># 字节 → 对象</span></span><br></pre></td></tr></table></figure><p>通常不仅仅是为了“保存到文件”，更多是为了“<strong>跨进程通信</strong>”。</p><ul><li><strong>序列化 (<code>dumps</code>)</strong>：把内存中复杂的对象（比如列表、字典、甚至你自定义的类实例）“拍扁”成一串<strong>字节流</strong>（Binary）。  </li><li><strong>反序列化 (<code>loads</code>)</strong>：把这串字节流重新“吹气”膨胀，恢复成内存中一模一样的对象。</li></ul><p><mark style="background:#fff88f">1. 一般用在进程间通信上：IPC</mark><br>大模型推理通常是多卡的。主进程（Master）负责分配任务，工作进程（Worker&#x2F;GPU）负责计算</p><ul><li><p>主进程把要执行的 <code>method_name</code> 和 <code>args</code> 用 <code>pickle.dumps</code> 变成字节，通过网络或管道（Pipe）发给 GPU 进程。</p></li><li><p>GPU 进程收到后用 <code>pickle.loads</code> 还原，然后执行对应的函数</p></li></ul><p><mark style="background:#fff88f">2. 分布式对象传输</mark><br>分布式训练或推理（Ray 框架或 Torch Distributed），对象需要在不同的物理机器之间传输。由于网线里<strong>只能传二进制数据</strong>，<code>pickle</code> 就充当了“打包员”</p><p><mark style="background:#fff88f">3. 保存模型状态&#x2F;检查点 (Checkpoint)</mark><br>当你训练了一个模型，或者计算出了一些复杂的特征向量（Embeddings），你可以把它们 <code>pickle.dump</code> 到磁盘上。下次启动程序，直接 <code>load</code> 回来，不需要重新计算</p><p><mark style="background:#affad1">pickle序列化的api</mark></p><ul><li><code>dumps</code> → 转字节（内存）</li><li><code>loads</code> → 从字节恢复</li><li><code>dump</code> → 存文件</li><li><code>load</code> → 读文件</li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> pickle</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">data = &#123;<span class="string">&quot;name&quot;</span>:<span class="string">&quot;liangji&quot;</span>, <span class="string">&quot;age&quot;</span>:<span class="number">18</span>&#125;</span><br><span class="line">byte_data = pickle.dumps(data)</span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;data.pkl&quot;</span>, <span class="string">&quot;wb&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    pickle.dump(data, f)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">restore_data = pickle.loads(byte_data)</span><br><span class="line"><span class="built_in">print</span>(restore_data)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;data.pkl&quot;</span>, <span class="string">&#x27;rb&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    loaded_data = pickle.load(f)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(loaded_data)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260517153505.png" alt="320"></p><h4 id="星号解包"><a href="#星号解包" class="headerlink" title="星号解包"></a>星号解包</h4><table><thead><tr><th align="left">符号</th><th align="left">名字</th><th align="left">作用</th></tr></thead><tbody><tr><td align="left"><code>*</code></td><td align="left">单星号</td><td align="left">解包<strong>位置参数</strong>（列表 &#x2F; 元组 &#x2F; 可迭代对象）</td></tr><tr><td align="left"><code>**</code></td><td align="left">双星号</td><td align="left">解包<strong>关键字参数</strong>（字典）</td></tr></tbody></table><blockquote><p>列表，元组&#x2F;可迭代对象 + * 是拆分成多个对象<br><img src="/images/Pasted%20image%2020260517151011.png" alt="514">、</p></blockquote><p>**双星号解包，<mark style="background:#fff88f">关键字参数《-》字典的类型转换</mark></p><blockquote><p>字典 + ** 拆分成关键字</p></blockquote><p><img src="/images/Pasted%20image%2020260517151304.png"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">func</span>(<span class="params">*args, **kwargs</span>):</span><br><span class="line">    <span class="built_in">print</span>(args) <span class="comment">#(1, 2, 3)</span></span><br><span class="line">    <span class="built_in">print</span>(kwargs) <span class="comment">#&#123;&#x27;name&#x27;: &#x27;liangji&#x27;, &#x27;age&#x27;: 18&#125;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">func(<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>, name=<span class="string">&quot;liangji&quot;</span>,age=<span class="number">18</span>)</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="classmethon-和-staticmethod"><a href="#classmethon-和-staticmethod" class="headerlink" title="@classmethon 和 @staticmethod"></a>@classmethon 和 @staticmethod</h2><table><thead><tr><th align="left">装饰器</th><th align="left">自动传入的参数</th><th align="left">能访问的内容</th><th align="left">典型用途</th></tr></thead><tbody><tr><td align="left"><code>@classmethod</code></td><td align="left"><code>cls</code>（类本身）</td><td align="left">类的属性、其他类方法</td><td align="left">创建实例、工厂方法、修改类属性</td></tr><tr><td align="left"><code>@staticmethod</code></td><td align="left">无</td><td align="left">类和实例都不能直接访问</td><td align="left">与类相关但不需要类信息的工具函数</td></tr></tbody></table><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>:</span><br><span class="line">    species = <span class="string">&quot;人类&quot;</span>  <span class="comment"># 类属性</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, name</span>):</span><br><span class="line">        <span class="variable language_">self</span>.name = name</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 1. 类方法：带 cls 参数</span></span><br><span class="line"><span class="meta">    @classmethod</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">create_from_dict</span>(<span class="params">cls, data</span>):</span><br><span class="line">        <span class="comment"># cls 就是 Person 这个类本身</span></span><br><span class="line">        <span class="keyword">return</span> cls(data[<span class="string">&quot;name&quot;</span>])  <span class="comment"># 用类创建实例</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 2. 静态方法：不带任何特殊参数</span></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">is_adult</span>(<span class="params">age</span>):</span><br><span class="line">        <span class="comment"># 纯工具函数，和 Person 类本身无关</span></span><br><span class="line">        <span class="keyword">return</span> age &gt;= <span class="number">18</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试</span></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># 用类方法创建实例</span></span><br><span class="line">    p1 = Person.create_from_dict(&#123;<span class="string">&quot;name&quot;</span>: <span class="string">&quot;小明&quot;</span>&#125;)</span><br><span class="line">    <span class="built_in">print</span>(p1.name)  <span class="comment"># 小明</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 用静态方法判断年龄</span></span><br><span class="line">    <span class="built_in">print</span>(Person.is_adult(<span class="number">20</span>))  <span class="comment"># True</span></span><br></pre></td></tr></table></figure><p><strong>什么时候用 <code>@classmethod</code>？</strong></p><ul><li>你需要在方法里<strong>创建这个类的实例</strong>（比如工厂模式）</li><li>你需要修改 &#x2F; 访问<strong>类属性</strong>（比如统计创建了多少个实例）</li><li>你需要调用类的其他方法</li></ul><p> <strong>什么时候用 <code>@staticmethod</code>？</strong></p><ul><li>方法逻辑和类本身<strong>完全无关</strong>，只是 “名义上属于这个类”</li><li>不需要访问类属性，也不需要访问实例属性</li></ul><h2 id="断言-assert"><a href="#断言-assert" class="headerlink" title="断言 assert"></a>断言 assert</h2><p>“设卡检查”</p><p><code>assert</code> 是一种调试辅助工具。它的逻辑非常简单粗暴：</p><ul><li><strong>如果条件为 <code>True</code></strong>：程序继续跑，像什么都没发生一样。</li><li><strong>如果条件为 <code>False</code></strong>：程序立即“原地爆炸”（抛出 <code>AssertionError</code> 异常），并停止运行。</li></ul><table><thead><tr><th><strong>语法</strong></th><th><strong>含义</strong></th></tr></thead><tbody><tr><td><strong><code>assert &lt;条件&gt;</code></strong></td><td>仅检查条件。</td></tr><tr><td><strong><code>assert &lt;条件&gt;, &lt;错误消息&gt;</code></strong></td><td>条件失败时，打印出你写的字符串，方便排查。</td></tr><tr><td><strong>性能提示</strong></td><td>如果在启动 Python 时使用了 <code>-O</code>（优化模式），断言会被<strong>全部忽略</strong>。</td></tr></tbody></table><p>在 <code>model_runner.py</code> 这种高性能代码中，区分这两种报错很重要：</p><ul><li><strong><code>if...raise...</code></strong>：用于处理<strong>预期内</strong>的错误（比如用户输入了一个不存在的文件名）。即使发布了，也要保留。</li><li><strong><code>assert</code></strong>：用于捕捉<strong>程序内部的逻辑错误</strong>（开发者认为“这绝对不可能发生”的情况）。它主要在开发和测试阶段起作用。</li></ul><h1 id="整体推理架构梳理"><a href="#整体推理架构梳理" class="headerlink" title="整体推理架构梳理"></a>整体推理架构梳理</h1><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">┌──────────────────────────────────────────────┐</span><br><span class="line">│ nano-vllm                                    │  ← 你写的</span><br><span class="line">│ 调度器 / BlockManager / 模型结构               │</span><br><span class="line">├──────────────────────────────────────────────┤</span><br><span class="line">│ PyTorch (torch.*)                             │  ← Python 深度学习框架</span><br><span class="line">│ F.linear / nn.Module / tensor 操作            │</span><br><span class="line">│ torch.cuda.CUDAGraph / dist.all_reduce         │</span><br><span class="line">├──────────────────────────────────────────────┤</span><br><span class="line">│ ATen + Torch Inductor                         │  ← PyTorch 的 C++ 后端</span><br><span class="line">│ PyTorch 内部算子实现 + codegen                 │</span><br><span class="line">├──────────────┬───────────────────────────────┤</span><br><span class="line">│ FlashAttention  │ Triton                      │  ← 自定义 CUDA kernel(算子)</span><br><span class="line">│ (预编译的 CUDA)  │ (DSL -&gt; CUDA kernel)        │</span><br><span class="line">├──────────────┴───────────────────────────────┤</span><br><span class="line">│ CUDA Runtime API                              │  ← 你问的&quot;cuda运行时库&quot;</span><br><span class="line">│ cudaMalloc / cudaMemcpy / cudaGraphLaunch     │</span><br><span class="line">│ cublas / cudnn / nccl                         │</span><br><span class="line">├──────────────────────────────────────────────┤</span><br><span class="line">│ CUDA Driver API                               │  ← 驱动层</span><br><span class="line">│ cuCtxCreate / cuModuleLoad / cuLaunchKernel   │</span><br><span class="line">├──────────────────────────────────────────────┤</span><br><span class="line">│ GPU (硬件)                                     │</span><br><span class="line">└──────────────────────────────────────────────┘</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260518193310.png"></p><hr><ul><li>前缀缓存prefix cache</li><li><strong>chunked prefill</strong> (scheduler)<ul><li>仅支持等待队列中第一个seq的chunked</li></ul></li><li>CUDA Graph</li><li>FlashAttention</li><li><strong>continuous batching</strong><ul><li><img src="/images/Pasted%20image%2020260518211421.png" alt="443"></li></ul></li><li><strong>张量并行</strong></li><li><strong>pagedattention</strong><ul><li>block_table作为页表，写入位置作为slot, blockmanager作为操作系统，kvcache是具体的物理内存，所以底层具体写入物理内存，用的是FlashAttention, 而页表管理则是靠blockmanager</li></ul></li></ul><h1 id="分模块解析"><a href="#分模块解析" class="headerlink" title="分模块解析"></a>分模块解析</h1><h2 id="sequence"><a href="#sequence" class="headerlink" title="sequence"></a>sequence</h2><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">//一个序列，prompt提示，请求</span></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">所以，一个sequence对象，本质上，代表着一个session对话的完整过程。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">sequence</span>&#123;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"><span class="type">int</span> block_size = <span class="number">256</span>; <span class="comment">//一块物理内存（一页）容纳256个token</span></span><br><span class="line"><span class="comment">//<span class="doctag">TODO:</span> counter, 全局计数器</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="comment">/*整体序列的信息*/</span></span><br><span class="line"><span class="type">int</span> seq_id; <span class="comment">//该prompt的唯一id</span></span><br><span class="line">SequenceStatus status; <span class="comment">//该条prompt的状态</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*具体token*/</span></span><br><span class="line"><span class="type">int</span> token_ids[]; <span class="comment">//具体的token ID 的数组</span></span><br><span class="line"><span class="type">int</span> last_token; <span class="comment">//最后一个token的ID</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*数量信息*/</span></span><br><span class="line"><span class="type">int</span> num_tokens; <span class="comment">//当前的token数量</span></span><br><span class="line"><span class="type">int</span> num_prompt_tokens; <span class="comment">//提示词部分的token数量</span></span><br><span class="line"><span class="type">int</span> num_cached_tokens; <span class="comment">//已经计算KVcache的token</span></span><br><span class="line"><span class="type">int</span> num_scheduled_tokens; <span class="comment">//本轮step准备处理的token数（prompt）</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*计算过程信息*/</span></span><br><span class="line"><span class="type">bool</span> is_prefill; <span class="comment">//是否已经预填充</span></span><br><span class="line">Block block_table[]; <span class="comment">//block列表，存放计算过程中的KVcache</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*模型回归结果的约束*/</span></span><br><span class="line"><span class="type">int</span> temperature; <span class="comment">//指定采样程度</span></span><br><span class="line"><span class="type">int</span> max_tokens; <span class="comment">//指定序列的最大输出长度</span></span><br><span class="line"><span class="type">int</span> ignore_eos; <span class="comment">//指定是否忽略结束符填充</span></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">&#125;</span><br></pre></td></tr></table></figure><h2 id="blockmanager"><a href="#blockmanager" class="headerlink" title="blockmanager"></a>blockmanager</h2><p>block里面存放序列的tokens，主要用于计算前缀cache</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">//定义一个block内存页，存放KVcache</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Block</span>&#123;</span><br><span class="line"><span class="type">int</span> block_id; <span class="comment">//block的唯一标识符</span></span><br><span class="line"><span class="type">int</span> ref_count; <span class="comment">//引用这个block的sequence的数量，用于前缀缓存prefill cache</span></span><br><span class="line"><span class="type">int</span> hash; <span class="comment">//该block的快速校验身份</span></span><br><span class="line"><span class="type">int</span> token_ids[]; <span class="comment">//实际存储的token ID们，占用的内存。</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">update</span><span class="params">()</span></span>; <span class="comment">//更新这个block的内容</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">reset</span><span class="params">()</span></span>; <span class="comment">//清空这个block的内存</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>BlockManager用于管理空闲Block内存的分配与回收</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">负责管理所有的block的申请与回收</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BlockManager</span>&#123;</span><br><span class="line"><span class="comment">/*全局信息*/</span></span><br><span class="line"><span class="type">int</span> block_size; <span class="comment">//每个block容纳多少token数</span></span><br><span class="line">Block blocks[]; <span class="comment">//一口气申请num_blocks个block个内存。</span></span><br><span class="line">dict hash_to_block_id; <span class="comment">//创建通过hash值快速找到block的字典查询方法</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*两条链表*/</span></span><br><span class="line">Block* free_block_ids; <span class="comment">//空闲block双向队列(链表)</span></span><br><span class="line">Block* used_block_ids; <span class="comment">//占用block集合</span></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 class="comment">//构造函数</span></span><br><span class="line"><span class="built_in">BlockManager</span>()&#123;</span><br><span class="line"><span class="comment">//</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _allocate_block();<span class="comment">//分配一个空闲block</span></span><br><span class="line"><span class="type">void</span> _deallocate_block();<span class="comment">//回收一个block</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">can_allocate</span><span class="params">(sequence)</span></span>; <span class="comment">//判断是否能分配block给sequence</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">allocate</span><span class="params">(sequence)</span></span>;<span class="comment">//给一个sequence分配block</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="scheduler"><a href="#scheduler" class="headerlink" title="scheduler"></a>scheduler</h2><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//调度器，负责提供下一个token</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Scheduler</span>&#123;</span><br><span class="line"><span class="type">int</span> max_num_seqs; <span class="comment">//每个step最多处理多少序列</span></span><br><span class="line"><span class="type">int</span> max_num_batched_tokens; <span class="comment">//每个step处理最多多少token</span></span><br><span class="line"><span class="type">int</span> eos; <span class="comment">//终止符token ID </span></span><br><span class="line"><span class="type">int</span> block_size; <span class="comment">//KVcache的块大小</span></span><br><span class="line">Sequence* waiting; <span class="comment">//等待任务队列</span></span><br><span class="line">Sequence* running; <span class="comment">//运行任务队列</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(Sequence)</span></span>; <span class="comment">//增加一个任务</span></span><br><span class="line"></span><br><span class="line">(Sequence*, <span class="type">bool</span>) <span class="built_in">schedule</span>();<span class="comment">//调度工作</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">preempt</span><span class="params">(Sequence)</span></span>;<span class="comment">//紧急淘汰运行中的任务到等待队列</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">postprocess</span><span class="params">(Sequence*)</span><span class="comment">//后处理，把模型的输出token加入对应的任务。</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="model-runner"><a href="#model-runner" class="headerlink" title="model_runner"></a>model_runner</h2><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">//负责执行模型推理的前向传播</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Model_runner</span>&#123;</span><br><span class="line"><span class="type">void</span> config;<span class="comment">//指定的该模型的运行配置</span></span><br><span class="line"><span class="type">int</span> block_size;<span class="comment">//KVcache的每个页的大小=256</span></span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> enforce_eager;<span class="comment">//是否开启CUDA Graph</span></span><br><span class="line"><span class="type">int</span> world_size; <span class="comment">//张量并行的并行通道数=GPU数量</span></span><br><span class="line"><span class="type">int</span> rank; <span class="comment">//当前GPU编号</span></span><br><span class="line">SIGNAL event; <span class="comment">//多进程同步信号</span></span><br><span class="line"></span><br><span class="line">self.model; <span class="comment">//用算子组成的模型，已经加载好了权重</span></span><br><span class="line">Samper samper;<span class="comment">//采样器，负责选择具体的预测token</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">capture_cudagraph</span><span class="params">()</span></span>;<span class="comment">//截取完整的CUDAkernel序列，固化为可以重放的graph，省略每次kernel launch的开销。</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">allocate_kv_cache</span><span class="params">()</span></span>;<span class="comment">//显存分配，根据剩余显存，动态计算能存放多少个block,并把显存分给对应的model的各层</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">run_model</span><span class="params">()</span></span>;<span class="comment">//运行一次模型推理，得到预测的token</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">run</span><span class="params">()</span></span>; <span class="comment">//实际完成整个链路，预填充，run_model, 采样</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="LLMEngine"><a href="#LLMEngine" class="headerlink" title="LLMEngine"></a>LLMEngine</h2><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">class</span> <span class="title">LLMEngine</span><span class="params">()</span></span>&#123;</span><br><span class="line">ps[]; <span class="comment">//张量并行的工作进程列表</span></span><br><span class="line">event[]; <span class="comment">//每个子进程的event，用于协调TP的同步</span></span><br><span class="line">moder_runner; <span class="comment">//定义一个rank0 显卡的moder_runner，主进程中持有</span></span><br><span class="line">tokenizer; <span class="comment">//分词器，text - token_id 的转换</span></span><br><span class="line">scheduler; <span class="comment">//调度器，</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">通过 nano-vllm 源码理解 API、Engine、模型算子与 KV Cache 管理，补充推理引擎中的 Python 与 PyTorch 用法。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="推理系统" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E6%8E%A8%E7%90%86%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="KV Cache" scheme="https://liangji-seu.github.io/tags/KV-Cache/"/>
    
    <category term="FlashAttention" scheme="https://liangji-seu.github.io/tags/FlashAttention/"/>
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="nano-vllm" scheme="https://liangji-seu.github.io/tags/nano-vllm/"/>
    
    <category term="vLLM" scheme="https://liangji-seu.github.io/tags/vLLM/"/>
    
    <category term="PyTorch" scheme="https://liangji-seu.github.io/tags/PyTorch/"/>
    
    <category term="TP" scheme="https://liangji-seu.github.io/tags/TP/"/>
    
  </entry>
  
  <entry>
    <title>infra 现代c++学习</title>
    <link href="https://liangji-seu.github.io/2026/06/14/infra-%E7%8E%B0%E4%BB%A3c++%E5%AD%A6%E4%B9%A0/"/>
    <id>https://liangji-seu.github.io/2026/06/14/infra-%E7%8E%B0%E4%BB%A3c++%E5%AD%A6%E4%B9%A0/</id>
    <published>2026-06-14T15:51:25.000Z</published>
    <updated>2026-10-03T10:34:31.629Z</updated>
    
    <content type="html"><![CDATA[<p>这个文档，主要用来记录学习现代c++的笔记，主要学习前4章的内容：</p><ul><li>类型推导</li><li>auto关键字</li><li>智能指针</li><li>右值引用<br>这几个章节，然后用c++重构nanovllm的引擎部分作为项目来练手</li></ul><h1 id="类型推导"><a href="#类型推导" class="headerlink" title="类型推导"></a>类型推导</h1><p>类型推导，就是<strong>编译器</strong>自动帮你<strong>推断变量是什么类型</strong>，<strong>不用你手动写类型</strong>。</p><p><img src="/images/Pasted%20image%2020260518095537.png" alt="237"></p><h2 id="c98和现代c-的区别"><a href="#c98和现代c-的区别" class="headerlink" title="c98和现代c++的区别"></a>c98和现代c++的区别</h2><p><img src="/images/Pasted%20image%2020260518095655.png" alt="258"></p><p>C++98只有一种类型推导规则：<strong>函数模板</strong></p><p>在c11中，增加了两条规则：</p><ul><li><mark style="background:#fff88f">auto</mark></li><li><mark style="background:#fff88f">decltype</mark></li></ul><p>C++14<strong>继续扩展</strong>了 auto 和 decltype 的使用情况</p><h2 id="模板类型推导"><a href="#模板类型推导" class="headerlink" title="模板类型推导"></a>模板类型推导</h2><h3 id="引用和指针上的类型推导"><a href="#引用和指针上的类型推导" class="headerlink" title="引用和指针上的类型推导"></a>引用和指针上的类型推导</h3><p>对于这样一个模板的类型推导</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(ParamType param)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="built_in">f</span>(expr);</span><br></pre></td></tr></table></figure><p>我们在给定表达式expr时，可以获得ParamType的类型，然后<strong>基于ParamType来推导T的类型</strong>。这就是类型推导。</p><p>但是这个有3种情况</p><h4 id="ParamType-是指针-（非通用）引用"><a href="#ParamType-是指针-（非通用）引用" class="headerlink" title="ParamType 是指针&#x2F;（非通用）引用"></a>ParamType 是指针&#x2F;（非通用）引用</h4><p>对于下面这三种情况</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp; param)</span></span>; <span class="comment">// param是一个引用类型</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// x是一个int， ParamType 是 int</span></span><br><span class="line"><span class="type">int</span> x = <span class="number">27</span>; </span><br><span class="line"><span class="built_in">f</span>(x) <span class="comment">// param = x</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// cx是一个const int, ParamType 是 const int&amp;</span></span><br><span class="line"><span class="comment">// T 推导为const int</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> cx = x; </span><br><span class="line"><span class="built_in">f</span>(cx) <span class="comment">// param = cx </span></span><br><span class="line"></span><br><span class="line"><span class="comment">// rx是const int的引用, ParamType 是 const int&amp;</span></span><br><span class="line"><span class="comment">// T 推到为const int</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span>&amp; rx = x; </span><br><span class="line"><span class="built_in">f</span>(rx) <span class="comment">// param = rx</span></span><br></pre></td></tr></table></figure><p>解析如下：<br><img src="/images/Pasted%20image%2020260518102206.png" alt="383"></p><p>规则总结：</p><ul><li><strong><code>const</code> 会被保留，并且 “吃掉” 到 <code>T</code> 里</strong><ul><li>传 <code>const int</code> &#x2F; <code>const int&amp;</code> → <code>T</code> 推为 <code>const int</code>，<code>param</code> 为 <code>const int&amp;</code></li><li>这样 <code>param</code> 不能被修改，保证了常量安全。</li></ul></li><li><strong>引用本身的 <code>&amp;</code> 会被忽略</strong><ul><li>传 <code>int&amp;</code> &#x2F; <code>const int&amp;</code> → 推导时 <code>&amp;</code> 消失，<code>T</code> 只看被引用对象的类型。</li><li>所以 <code>T</code> <strong>永远不会被推成引用类型</strong>，只会推成普通类型（带不带 <code>const</code> 看情况）。</li></ul></li></ul><h4 id="ParamType-是-（通用）引用"><a href="#ParamType-是-（通用）引用" class="headerlink" title="ParamType 是 （通用）引用"></a>ParamType 是 （通用）引用</h4><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp;&amp; param)</span></span>; <span class="comment">// param现在是一个通用的引用</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> x = <span class="number">27</span>; <span class="comment">// 和之前一样</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> cx = x; <span class="comment">// 和之前一样</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span>&amp; rx = x; <span class="comment">// 和之前一样</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">f</span>(x); <span class="comment">// x是左值，所以T是int&amp;</span></span><br><span class="line"><span class="comment">// param的类型也是int&amp;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">f</span>(cx); <span class="comment">// cx是左值，所以T是const int&amp;</span></span><br><span class="line"><span class="comment">// param的类型也是const int&amp;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">f</span>(rx); <span class="comment">// rx是左值，所以T是const int&amp;</span></span><br><span class="line"><span class="comment">// param的类型也是const int&amp;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">f</span>(<span class="number">27</span>); <span class="comment">// 27是右值，所以T是int</span></span><br><span class="line"><span class="comment">// 所以param的类型是int&amp;&amp;</span></span><br></pre></td></tr></table></figure><ul><li><p><strong>如果 expr 是一个左值</strong>， T 和 ParamType(T&amp;&amp;) 都会<strong>被推导成左值引用</strong>。这有些不同寻常。</p><ul><li>第一，这是<strong>模板类型 T</strong> 被<strong>推导成一个引用的唯一情况</strong>。</li><li>第二，尽管 ParamType 利用右值引用的语法来进行推导，但是他最终推导出来的类型是左值引用。</li></ul></li><li><p><strong>如果 expr 是一个右值</strong>，那么就执行“普通”的法则（第一种情况）</p></li></ul><p><mark style="background:#fff88f">简单总结来说就是</mark>：<br><img src="/images/Pasted%20image%2020260518102837.png"></p><blockquote><p><strong>引用</strong>的本质，是别名，就是两个符号，对应同一块内存</p></blockquote><table><thead><tr><th align="left">特性</th><th align="left">引用（<code>int&amp;</code>）</th><th align="left">指针（<code>int*</code>）</th></tr></thead><tbody><tr><td align="left"><strong>本质</strong></td><td align="left">别名</td><td align="left">存储地址的变量</td></tr><tr><td align="left"><mark style="background:#fff88f"><strong>必须初始化吗？</strong></td><td align="left">✅ 必须，且只能绑定到一个对象</td><td align="left">❌ 可以先声明后赋值    </mark></td></tr><tr><td align="left"><mark style="background:#fff88f"><strong>能重新绑定吗？</strong></td><td align="left">❌ 绑定</mark>后不能改，只能改绑定对象的值</td><td align="left">✅ 可以随时指向别的地址</td></tr><tr><td align="left"><mark style="background:#fff88f"><strong>是否可能为空？</strong></mark></td><td align="left">❌ 不存在空引用（UB 除外）</td><td align="left">✅ 可以是 <code>nullptr</code></td></tr><tr><td align="left"><strong>语法</strong></td><td align="left">直接用 <code>.</code> 访问成员</td><td align="left">要用 <code>-&gt;</code> 访问成员</td></tr><tr><td align="left"><strong>底层实现</strong></td><td align="left"><mark style="background:#fff88f">通常用指针实现</mark>，但编译器帮你隐藏了解引用</td><td align="left">直接操作地址</td></tr></tbody></table><blockquote><p>什么是左值，右值<br><strong>左值</strong>：有名字、能取地址、可以出现在赋值号左边的值<br>比如变量 <code>x</code>、<code>cx</code>、<code>rx</code>，都有名字，能 <code>&amp;x</code> 取地址，是左值<br><br><strong>右值</strong>：临时的、没有名字、用完就消失的值，只能出现在赋值号右边<br>比如字面量 <code>27</code>、表达式返回的临时对象 <code>x+1</code>，都没有名字，不能取地址，是右值</p><p>一个简单判断方法：<strong>能对它取 <code>&amp;</code> 的，就是左值；不能的就是右值。</strong></p></blockquote><p>所以x, cx, rx 都是左值，但是27是临时对象，所以是右值</p><blockquote><p><strong>左值引用</strong>：Type&amp; , 绑定到左值上<br><img src="/images/Pasted%20image%2020260518103601.png"><br><strong>右值引用</strong>：Type&amp;&amp;, 只能绑定到右值，目的是为了支持移动语义（<code>std::move</code>）<br><img src="/images/Pasted%20image%2020260518103554.png"></p></blockquote><p><mark style="background:#fff88f">总结</mark></p><blockquote><p><mark style="background:#fff88f">通用引用</mark> T&amp;&amp;<br>只有在模板参数推导的场景下，才叫通用引用，能同时绑定左值和右值<br><strong>传递左值</strong>给通用引用：<br><code>T</code> 被推导成 <code>Type&amp;</code>，<code>ParamType</code> 变成 <code>Type&amp; &amp;&amp;</code> 这里触发了引用折叠规则：<strong><code>&amp; + &amp;&amp; → &amp;</code></strong>，所以最终 <code>ParamType</code> 是 <code>Type&amp;</code>（左值引用）<br><strong>传递右值</strong>给通用引用：<code>T</code> 被推导成 <code>Type</code>，<code>ParamType</code> 变成 <code>Type&amp;&amp;</code>（右值引用）</p></blockquote><h4 id="ParamType-既不是指针也不是引用（按值传递）"><a href="#ParamType-既不是指针也不是引用（按值传递）" class="headerlink" title="ParamType 既不是指针也不是引用（按值传递）"></a>ParamType 既不是指针也不是引用（按值传递）</h4><p>当ParamType既不是指针，也不是引用，我们把他处理成pass-by-value</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T param)</span></span>;</span><br></pre></td></tr></table></figure><p>这就意味着 param 就是完全传给他的参数的<strong>一份拷贝</strong>,一个完全新的对象</p><p>规则如下：</p><ul><li><strong>忽略引用特性</strong>：如果传入的 <code>expr</code> 是个引用，推导时会直接把 <code>&amp;</code> 去掉，只看底层的类型。</li><li><strong>忽略顶层 <code>const</code>&#x2F;<code>volatile</code></strong>：如果去掉引用后，类型带有 <code>const</code>&#x2F;<code>volatile</code>（顶层），也会被忽略。</li></ul><p>因为本质上就是形参拷贝了，所以不关心这些原本的性质了。</p><p>举例(特殊)：<br><img src="/images/Pasted%20image%2020260518104329.png" alt="396"></p><ul><li><code>const char*</code>：修饰的是<strong>指针指向的内容</strong>，表示 <code>ptr</code> 指向的 <code>char</code> 是不可修改的（底层 const）。</li><li><code>* const ptr</code>：修饰的是<strong>指针本身</strong>，表示 <code>ptr</code> 这个变量本身的值（地址）不可修改（顶层 const）。</li></ul><p>会<strong>忽略顶层的 <code>const</code>&#x2F;<code>volatile</code></strong>，但会保留底层的 <code>const</code>&#x2F;<code>volatile</code>。</p><p>因为 <code>param</code> 是原对象的拷贝，原对象 “本身不可修改” 的特性（顶层 const）对拷贝出来的新对象没有约束，所以会被去掉；但 “指向的对象不可修改” 的特性（底层 const）是类型的一部分，必须保留</p><p><em><em>所以，T被推导成 const char</em>, 表示这块内存不可修改，但是指针本身是可以重定向的</em>*。</p><h3 id="数组参数"><a href="#数组参数" class="headerlink" title="数组参数"></a>数组参数</h3><p>一般我们用的时候，我们把数组当作指向第一个元素的指针。<br><img src="/images/Pasted%20image%2020260518104642.png"></p><p>实际上，<code>name</code>的类型是 <code>const char[13]</code>, 而不是<code>const char*</code>,  但是因为数组到指针的退化规则，代码会被正常编译。</p><p>但是如果数组类型，被传递给一个模板参数，类型推导会怎么样呢？<br><img src="/images/Pasted%20image%2020260518104836.png"></p><p>因为数组参数声明会被当做指针参数，传递给模板函数的按值传递的<strong>数组参数</strong>会被<strong>退化成指针类型</strong>。这就意味着在模板 f 的调用中，模板参数 T 被推导成 <code>const char*</code> ：<br><img src="/images/Pasted%20image%2020260518105106.png"></p><h4 id="特例"><a href="#特例" class="headerlink" title="特例"></a>特例</h4><p><img src="/images/Pasted%20image%2020260518105124.png"></p><p><strong>T最后推导出来的实际的类型就是数组</strong> <code>const char [13]</code></p><p>函数 f 的参数（数组的引用）被推导成了 <code>const char(&amp;)[13]</code></p><p>这个特例的使用</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">//在编译的时候返回数组的长度（数组参数没有名字，</span></span><br><span class="line"><span class="comment">//因为只关心数组包含的元素的个数）</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T, <span class="type">size_t</span> N&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="type">size_t</span> <span class="title">arraySize</span><span class="params">(T (&amp;)[N])</span> <span class="keyword">noexcept</span></span>&#123;</span><br><span class="line"><span class="keyword">return</span> N;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>声明数组的引用可以使的创造出一个推导出一个<strong>数组包含的元素长度</strong>的模板<img src="/images/Pasted%20image%2020260518105619.png"></p><p>定义为 <code>constexpr</code> 说明函数<strong>可以在编译的时候得到其返回值</strong><br><img src="/images/Pasted%20image%2020260518105840.png"></p><h3 id="函数参数"><a href="#函数参数" class="headerlink" title="函数参数"></a>函数参数</h3><p>数组不是唯一可以退化成指针的东西，函数类型也可以被退化成函数指针。<br><img src="/images/Pasted%20image%2020260518105925.png"></p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><img src="/images/Pasted%20image%2020260518110305.png"></p><h2 id="auto类型推导"><a href="#auto类型推导" class="headerlink" title="auto类型推导"></a>auto类型推导</h2><p>了解了模板类型推导，auto的类型推导基本所有的内容。</p><p><strong>因为除了第一个例外，auto类型推导就是模板类型推导</strong></p><p>模板类型推导和 auto 类型推导是有<mark style="background:#fff88f">一个直接的映射</mark></p><p>他们的对应关系是：</p><table><thead><tr><th align="left">auto 写法</th><th align="left">对应的模板形式</th></tr></thead><tbody><tr><td align="left"><code>auto x = expr;</code></td><td align="left"><code>template&lt;typename T&gt; void f(T param); f(expr);</code></td></tr><tr><td align="left"><code>const auto cx = expr;</code></td><td align="left"><code>template&lt;typename T&gt; void f(const T param); f(expr);</code></td></tr><tr><td align="left"><code>auto&amp; rx = expr;</code></td><td align="left"><code>template&lt;typename T&gt; void f(T&amp; param); f(expr);</code></td></tr><tr><td align="left"><code>auto&amp;&amp; urx = expr;</code></td><td align="left"><code>template&lt;typename T&gt; void f(T&amp;&amp; param); f(expr);</code></td></tr><tr><td align="left">也就是说：</td><td align="left"></td></tr></tbody></table><ul><li><code>auto</code> 就相当于模板里的 <code>T</code></li><li><code>auto</code> 前面 &#x2F; 后面的 <code>const</code>&#x2F;<code>&amp;</code>&#x2F;<code>&amp;&amp;</code>，就相当于模板里的 <code>ParamType</code></li></ul><h3 id="左值引用"><a href="#左值引用" class="headerlink" title="左值引用"></a>左值引用</h3><p><mark style="background:#fff88f">（auto&amp; a &#x3D; xxx， const保留，&amp;丢）</mark><br>T&amp; x &#x3D; expr<br>和模板类型推导规则一样：</p><ul><li>保留 <code>expr</code> 的 <code>const</code> 特性</li><li>忽略 <code>expr</code> 本身的引用特性&amp;</li></ul><h3 id="通用引用"><a href="#通用引用" class="headerlink" title="通用引用"></a>通用引用</h3><p>T&amp;&amp; x &#x3D; expr<br>和模板类型推导规则一样：</p><ul><li><code>expr</code> 是左值 → auto 推成左值引用（<strong>引用折叠</strong>）</li><li><code>expr</code> 是右值 → auto 推成普通类型，最终是右值引用</li></ul><h3 id="按值传递"><a href="#按值传递" class="headerlink" title="按值传递"></a>按值传递</h3><p><mark style="background:#fff88f">（auto a &#x3D; xxx， const，&amp;全丢）</mark><br>T x &#x3D;expr<br>&#96;auto x &#x3D; expr;</p><ul><li>忽略 <code>expr</code> 的引用特性</li><li>忽略顶层 <code>const</code>&#x2F;<code>volatile</code>(因为拷贝出来的对象，没有约束)</li><li>底层 <code>const</code> 会保留</li></ul><h3 id="唯一的例外"><a href="#唯一的例外" class="headerlink" title="唯一的例外"></a>唯一的例外</h3><p>这是 auto 和模板推导唯一不一样的地方：</p><ul><li>模板推导不支持<strong>初始化列表</strong> <code>{}</code></li><li>auto 支持，并且会把 <code>auto x = {expr};</code> 推成 <code>std::initializer_list&lt;T&gt;</code></li></ul><p><img src="/images/Pasted%20image%2020260518111919.png"></p><h5 id="初始化列表"><a href="#初始化列表" class="headerlink" title="初始化列表{}"></a>初始化列表{}</h5><p>这个是c++11的新特性</p><p><strong>用{} 给变量赋值，就是初始化列表</strong></p><p>c98的写法：<br><img src="/images/Pasted%20image%2020260518112601.png"></p><p>c11之后<br><img src="/images/Pasted%20image%2020260518112616.png"><br>这种 <code>{}</code> 形式，就叫 <strong>初始化列表</strong>。</p><p>对于<code>auto x = {1,2,3}</code><br><code>auto</code>推导出来的类型是：<code>std::initializer_list&lt;int&gt;</code></p><p>它是一个<strong>C++11 新增的模板类型</strong>，用来表示 “一堆值的集合”。</p><p>总结，auto对{}的推导为：<br><img src="/images/Pasted%20image%2020260518112806.png"><br><img src="/images/Pasted%20image%2020260518112851.png"><br>而模板是不能推导{}的，这就是auto和模板推导唯一的区别了</p><p>而<mark style="background:#fff88f">初始化列表这个类型，是c++11专门给容器，函数传参涉及的，统一批量传值工具</mark>， 其本质，就是一个临时，只读，轻量的值列表，目的仅仅用来初始化和传参</p><p><img src="/images/Pasted%20image%2020260518113050.png" alt="251"></p><p>还有一个好处，<mark style="background:#fff88f">就是让传参的形式统一：</mark></p><p><img src="/images/Pasted%20image%2020260518113136.png" alt="206"></p><h2 id="decltype"><a href="#decltype" class="headerlink" title="decltype"></a>decltype</h2><p>给定一个变量名或者表达式， <code>decltype</code> 会告诉你这个<strong>变量名或表达式</strong>的<strong>类型</strong>，不会做任何模板 &#x2F;auto 那样的推导修改</p><p>和 <code>auto</code> 对比一下：</p><ul><li><code>auto</code>：会做推导，忽略引用、顶层 const（按值传递时）</li><li><code>decltype</code>：<strong>不推导、不修改</strong>，表达式是什么类型，就返回什么类型。</li></ul><p><img src="/images/Pasted%20image%2020260518132715.png" alt="447"></p><blockquote><p>注意，[]操作符的返回值是引用</p></blockquote><h3 id="（c-11）核心的用途：尾置返回类型"><a href="#（c-11）核心的用途：尾置返回类型" class="headerlink" title="（c++11）核心的用途：尾置返回类型"></a>（c++11）核心的用途：尾置返回类型</h3><p>在c++11之前，我写一个函数，接受一个容器，返回索引i的引用</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">？？？ <span class="built_in">authAndAccess</span>(Container&amp; c, Index i)&#123;</span><br><span class="line"><span class="keyword">return</span> c[i];<span class="comment">//返回一个引用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现，如果我想返回一个引用，肯定函数的类型是<code>ParamType&amp; </code></p><p>这就带来一个问题，我这是模板函数，<strong>我还不知道输入的容器的类型呢</strong>。这就没办法</p><p><strong>c++11里面，用<code>decltype</code> + <code>尾置返回类型</code>解决</strong></p><p>C++11 引入了 <code>-&gt; decltype(...)</code> 语法，把返回类型写在函数参数后面：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> Container, <span class="keyword">typename</span> Index&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">authandaccess</span><span class="params">(Container&amp; c, Index i)</span> -&gt; <span class="title">decltype</span><span class="params">(c[i])</span> </span>&#123;</span><br><span class="line"><span class="keyword">return</span> c[i]; <span class="comment">//容器c用[]运算符获得的c[i]，是引用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><code>auto</code> 只是占位符，真正的返回类型由 <code>decltype(c[i])</code> 决定</li><li><code>decltype(c[i])</code> 会原封不动地返回 <code>c[i]</code> 的类型（比如 <code>int&amp;</code>）</li></ul><h3 id="c-14的用法：自动用decltype规则推导"><a href="#c-14的用法：自动用decltype规则推导" class="headerlink" title="c++14的用法：自动用decltype规则推导"></a>c++14的用法：自动用decltype规则推导</h3><p>c++14进一步简化了上面的写法，引入<code>decltype(auto)</code></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> Container, <span class="keyword">typename</span> Index&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">decltype</span>(<span class="keyword">auto</span>) <span class="title">authandaccess</span><span class="params">(Container&amp; c, Index i)</span></span>&#123;</span><br><span class="line"><span class="keyword">return</span> c[i]; <span class="comment">//容器c用[]运算符获得的c[i]，是引用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>像 <code>auto</code> 一样自动推导返回类型</li><li>但<strong>使用 <code>decltype</code> 的推导规则</strong>（不忽略引用、const）</li></ul><blockquote><p>疑问：既然都是推导类型，那为什么直接用auto不行？</p><p>因为c[i]返回的是xxx类型的引用，比如int&amp;, 如果用auto进行模板规则的推导，会走左值推导，得出int的结果</p><p>而 <code>decltype(auto)</code> 会保留 <code>int&amp;</code>，完美解决这个问题。</p></blockquote><h3 id="decltype的坑：（）会改变结果"><a href="#decltype的坑：（）会改变结果" class="headerlink" title="decltype的坑：（）会改变结果"></a>decltype的坑：（）会改变结果</h3><p><img src="/images/Pasted%20image%2020260518133849.png" alt="449"></p><ul><li>对变量名直接用 <code>decltype</code>，返回它的声明类型</li><li>对变量加括号，就变成了 “左值表达式”，<code>decltype</code> 会返回 <code>T&amp;</code></li></ul><p><img src="/images/Pasted%20image%2020260518133940.png"><br>所以，如果是返回局部变量的值，就不能加（）, 否则会返回引用。</p><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p><img src="/images/Pasted%20image%2020260518134046.png"></p><h2 id="auto关键字"><a href="#auto关键字" class="headerlink" title="auto关键字"></a>auto关键字</h2><p>一些 auto 类型的推导结果虽然完成符合规定的算法，但是从程序员的角度来看是错误的</p><p>如何引导 auto 得到正确的结果是很重要</p><p>这里包含了所有 auto 的输入和输出</p><h3 id="优先使用auto，不是显式类型声明"><a href="#优先使用auto，不是显式类型声明" class="headerlink" title="优先使用auto，不是显式类型声明"></a>优先使用auto，不是显式类型声明</h3><p>这里说明的是auto解决了<strong>老式c++里面变量声明</strong>的两大痛点：</p><ul><li>复杂类型写起来麻烦</li><li>容易忘记初始化<br><img src="/images/Pasted%20image%2020260518141516.png" alt="469"></li></ul><p>auto的解决办法是：</p><ul><li>自动推导类型，不用写复杂名字</li><li>强制初始化，从根源上避免未定义行为<br><img src="/images/Pasted%20image%2020260518141607.png"></li></ul><h4 id="auto可以持有编译器才知道的类型（Lambda）"><a href="#auto可以持有编译器才知道的类型（Lambda）" class="headerlink" title="auto可以持有编译器才知道的类型（Lambda）"></a>auto可以持有编译器才知道的类型（Lambda）</h4><p>Lambda 表达式的类型是编译器内部生成的匿名类型<br><img src="/images/Pasted%20image%2020260518141904.png" alt="381"></p><ul><li><code>auto</code> 会直接推导为 Lambda 的原生类型，<strong>没有额外开销，调用更快</strong></li><li><code>std::function</code> 是一个类型擦除的包装器，会有额外的内存开销和调用成本，而且代码写起来更繁琐</li></ul><h4 id="auto-能避免-“类型截断-不匹配”-的隐性错误"><a href="#auto-能避免-“类型截断-不匹配”-的隐性错误" class="headerlink" title="auto 能避免 “类型截断 &#x2F; 不匹配” 的隐性错误"></a><code>auto</code> 能避免 “类型截断 &#x2F; 不匹配” 的隐性错误</h4><p><img src="/images/Pasted%20image%2020260518142133.png"></p><p><img src="/images/Pasted%20image%2020260518142225.png"></p><h4 id="重构代码更简单"><a href="#重构代码更简单" class="headerlink" title="重构代码更简单"></a>重构代码更简单</h4><p><img src="/images/Pasted%20image%2020260518142320.png"></p><h3 id="auto不能用的特殊情况"><a href="#auto不能用的特殊情况" class="headerlink" title="auto不能用的特殊情况"></a>auto不能用的特殊情况</h3><p><mark style="background:#fff88f"><strong>代理类</strong></mark></p><p><strong>auto 不是万能的，遇到「代理类」这类特殊场景，它会推导出错误类型，这时候需要用<code>static_cast</code>手动修正</strong></p><p><img src="/images/Pasted%20image%2020260518144242.png"></p><p>除了这个，还有其他情况，也有有代理类来做优化<br><img src="/images/Pasted%20image%2020260518145012.png" alt="397"><br><img src="/images/Pasted%20image%2020260518145518.png" alt="389"></p><h4 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h4><h5 id="显示类型初始化原则"><a href="#显示类型初始化原则" class="headerlink" title="显示类型初始化原则"></a>显示类型初始化原则</h5><p>用 <code>static_cast</code> 强制把表达式转换成你想要的类型，再给 <code>auto</code> 推导：<img src="/images/Pasted%20image%2020260518145332.png"></p><ul><li><code>static_cast&lt;bool&gt;</code> 会把代理类转换成真正的 <code>bool</code></li><li><code>auto</code> 再推导出 <code>bool</code>，彻底避免持有代理类</li></ul><p><strong>总结</strong>：<br><code>auto</code> 会推导出表达式的真实类型，而不是你 “期望” 的类型。当遇到代理类这类特殊场景时，需要用 <code>static_cast</code> 显式转换，把它变成你想要的类型，再交给 <code>auto</code>。</p><h2 id="使用现代C"><a href="#使用现代C" class="headerlink" title="使用现代C++"></a>使用现代C++</h2><h3 id="统一初始化"><a href="#统一初始化" class="headerlink" title="统一初始化{}"></a>统一初始化{}</h3><p>这里就是统一用{}来初始化。</p><p>C++11之前，初始化语法比较混乱。</p><p><mark style="background:#affad1">为什么需要{}来统一初始化</mark><br>原来的初始化是这样子的：</p><p><img src="/images/Pasted%20image%2020260518150114.png"><br>很乱。</p><p>我们用{}，被称为统一初始化，因为<strong>它可以用于任何地方</strong>：</p><ul><li>非静态数据成员指定默认值</li><li>初始化不可复制的对象（如 <code>std::atomic</code>）等</li></ul><p><mark style="background:#fff88f">优势 1：阻止“变窄转换”（Narrowing Conversions）</mark><br><img src="/images/Pasted%20image%2020260518150340.png" alt="491"><br><img src="/images/Pasted%20image%2020260518150536.png" alt="409"></p><p><mark style="background:#fff88f">优势 2：免疫 C++ 史上最令人头疼的解析（Most Vexing Parse）</mark><br>在 C++ 中有一条让无数新手掉坑的规则：“<strong>任何可以解析为函数声明的东西，都会被解析为函数声明</strong>”。<br><img src="/images/Pasted%20image%2020260518150352.png" alt="531"></p><h5 id="特殊情况"><a href="#特殊情况" class="headerlink" title="特殊情况"></a>特殊情况</h5><p>不是所有情况的初始化，都可以使用{}来初始化的。</p><p>因为{1，2，3}这种，本质上就是一个初始化列表的对象。</p><p>但是，如果你的类初始化的参数里面，有一个接收初始化列表的，这样在调用构造函数的时候，编译器就会优先匹配它。</p><p><img src="/images/Pasted%20image%2020260518151149.png" alt="396"></p><p><img src="/images/Pasted%20image%2020260518151205.png" alt="469"></p><h5 id="默认构造函数与空"><a href="#默认构造函数与空" class="headerlink" title="默认构造函数与空{}"></a>默认构造函数与空{}</h5><p>如果一个类既有默认构造函数，又有 <code>std::initializer_list</code> 构造函数，那么<strong>空的 <code>{}</code> 意味着调用默认构造函数，而不是传入一个空的<code>initializer_list</code></strong>。<br><img src="/images/Pasted%20image%2020260518151315.png" alt="474"></p><h3 id="nullptr"><a href="#nullptr" class="headerlink" title="nullptr"></a>nullptr</h3><p>现代c++, 如果想要表示空指针，永远只用nullptr</p><p>抛弃了0, NULL </p><h4 id="抛弃0，NULL的原因"><a href="#抛弃0，NULL的原因" class="headerlink" title="抛弃0，NULL的原因"></a>抛弃0，NULL的原因</h4><p><mark style="background:#affad1">根本原因：类型冲突</mark><br>c++里面，0，NULL， 本质上都是int，都不是指针类型。所以在函数重载的时候引发灾难<br><img src="/images/Pasted%20image%2020260518152202.png" alt="492"></p><p>当你写下 <code>f(NULL)</code> 时，你的本意是想调用指针版本的函数，但编译器却把它当成整数传给了 <code>f(int)</code>。这不仅违背直觉，还可能导致难以察觉的 Bug。</p><p>而 <code>nullptr</code> 的优势：<br><code>nullptr</code> 的类型是 <code>std::nullptr_t</code>，它可以隐式转换为<strong>任何</strong>指针类型（如 <code>int*</code>, <code>Widget*</code> 等），但它<strong>绝对不会</strong>被视为一个整数</p><p><img src="/images/Pasted%20image%2020260518152248.png" alt="461"></p><p><mark style="background:#affad1">模板类型推导失效</mark><br><img src="/images/Pasted%20image%2020260518152353.png" alt="459"></p><p><mark style="background:#affad1">代码可读性</mark><br>使用 <code>nullptr</code> 能让代码意图瞬间清晰。<br><img src="/images/Pasted%20image%2020260518152425.png"></p><h3 id="声明别名using"><a href="#声明别名using" class="headerlink" title="声明别名using"></a>声明别名using</h3><p>在原来的c98里面，我们使用typedef来给类型起别名</p><p>但是在现代c++中，使用using 别名声明</p><p>因为 <code>typedef</code> 在泛型编程（模板）中存在着致命的设计缺陷</p><h4 id="typedef的缺陷"><a href="#typedef的缺陷" class="headerlink" title="typedef的缺陷"></a>typedef的缺陷</h4><p><mark style="background:#affad1">可读性：尤其是处理函数指针的时候</mark></p><p>在底层开发或系统级编程中，我们经常需要<strong>处理回调函数</strong>或<strong>中断处理函数</strong></p><ul><li>用 <code>typedef</code> 定义函数指针的语法非常反人类，名字被“埋”在了中间；</li><li>而 <code>using</code> 则保持了清晰的“左边是名字，右边是值”的直觉逻辑。<br><img src="/images/Pasted%20image%2020260518152934.png" alt="465"></li></ul><p>正常回调的使用案例, 写起来更加简洁</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;vector&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string">&lt;string&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Button</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="comment">//typedef void (*ClickCallback)(int, int);</span></span><br><span class="line"><span class="keyword">using</span> ClickCallback = <span class="built_in">void</span>(*)(<span class="type">int</span>, <span class="type">int</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">//绑定回调函数</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">setClick</span><span class="params">(ClickCallback cb)</span> </span>&#123;</span><br><span class="line"><span class="keyword">this</span>-&gt;cb = cb;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//触发回调</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">click</span><span class="params">()</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (cb)</span><br><span class="line"><span class="built_in">cb</span>(<span class="number">100</span>, <span class="number">200</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">ClickCallback cb;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">onClick</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span> </span>&#123;</span><br><span class="line">cout &lt;&lt; <span class="string">&quot;click callback&quot;</span> &lt;&lt; endl;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">Button btn;</span><br><span class="line"></span><br><span class="line"><span class="comment">//绑定回到</span></span><br><span class="line">btn.<span class="built_in">setClick</span>(onClick);</span><br><span class="line">btn.<span class="built_in">click</span>();</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><p><mark style="background:#affad1">模板别名的使用</mark><br>这是 <code>using</code> 碾压 <code>typedef</code> 的根本原因。</p><p>当你在实现<strong>自定义的容器</strong>（比如某种基于智能指针的链表、或者自定义的 HashMap）时，你通常<strong>希望给一个带有自定义分配器的模板类</strong>起个<strong>简短的</strong>名字。</p><p><img src="/images/Pasted%20image%2020260518161554.png" alt="433"></p><p><strong>但是typedef却不支持直接模板化</strong></p><p>所以，你不能写成：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">templete&lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="keyword">typedef</span> sdt::list&lt;T, std::allocator&lt;T&gt;&gt; MyCustomList;</span><br></pre></td></tr></table></figure><p>根本原因 ——<strong><code>typedef</code> 不支持模板别名, C98就不支持，而 <code>using</code> 支持</strong>，</p><p>C++11 引入的 <code>using</code> 别名声明，天然支持模板化，也就是我们常说的<strong>模板别名（Alias Templates）</strong></p><hr><p><mark style="background:#affad1">终结 <code>typename</code> 和 <code>::type</code> 的噩梦</mark></p><p><img src="/images/Pasted%20image%2020260518162302.png" alt="389"><br><img src="/images/Pasted%20image%2020260518162158.png" alt="392"></p><blockquote><p>注意这里，使用这个类型的时候，必须要typename xxx，编译器才能通过，直到这是你的模板。</p></blockquote><p>你在模板里写 <code>MyCustomList_C98&lt;T&gt;::type</code> 时，编译器会一脸懵</p><ul><li><code>MyCustomList_C98&lt;T&gt;</code> 是个模板，编译器不知道它里面的 <code>::type</code> 是<strong>类型</strong>，还是<strong>静态成员变量</strong></li><li>比如它可能是 <code>int</code>，也可能是一个静态的 <code>int</code> 变量名，编译器没法判断</li></ul><p>这种 “依赖于模板参数 <code>T</code>，编译器无法直接判断的名字”，就叫<strong>依赖类型（Dependent Type）</strong></p><blockquote><p>是因为T未知，编译阶段还没有实例，自然也就没有类型。</p></blockquote><p>而使用<code>using</code> 支持直接定义模板别名，编译器<strong>100% 确定它就是个类型</strong></p><p><img src="/images/Pasted%20image%2020260518162522.png" alt="393"></p><table><thead><tr><th align="left">写法</th><th align="left">模板里使用时</th><th align="left">可读性</th><th align="left">编译器理解成本</th></tr></thead><tbody><tr><td align="left"><code>typedef + 结构体</code></td><td align="left"><code>typename MyCustomList_C98&lt;T&gt;::type</code></td><td align="left">差</td><td align="left">高，必须你加 <code>typename</code> 提示</td></tr><tr><td align="left"><code>using</code> 模板别名</td><td align="left"><code>MyCustomList&lt;T&gt;</code></td><td align="left">好</td><td align="left">低，编译器直接知道是类型</td></tr></tbody></table><p><mark style="background:#affad1">C++14 里全面引入 <code>using</code> 模板别名</mark></p><p><strong>C++ 标准库为了解决 <code>typedef</code> 和 <code>typename</code>&#x2F;<code>::type</code> 的麻烦，在 C++14 里全面引入 <code>using</code> 模板别名</strong>，把类型特征（Type Traits）的写法彻底简化了</p><p>C++11 引入了 <code>&lt;type_traits&gt;</code> 头文件，用来操作类型（比如移除引用、移除 const、判断是否是指针等），但它的实现是基于 <code>typedef</code> 的老路子，导致使用起来非常麻烦</p><p><img src="/images/Pasted%20image%2020260518163559.png"></p><p>相当于给你省略了创建别名的方法，</p>]]></content>
    
    
    <summary type="html">围绕类型推导、auto、智能指针、右值引用与现代 C++ 特性，配合 nanovllm 重构记录工程实践。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="工程基础" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%B7%A5%E7%A8%8B%E5%9F%BA%E7%A1%80/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="C++" scheme="https://liangji-seu.github.io/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>infra 基于cuda自制llama大模型推理框架</title>
    <link href="https://liangji-seu.github.io/2026/06/14/infra-%E5%9F%BA%E4%BA%8Ecuda%E8%87%AA%E5%88%B6llama%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E6%A1%86%E6%9E%B6/"/>
    <id>https://liangji-seu.github.io/2026/06/14/infra-%E5%9F%BA%E4%BA%8Ecuda%E8%87%AA%E5%88%B6llama%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E6%A1%86%E6%9E%B6/</id>
    <published>2026-06-14T15:51:25.000Z</published>
    <updated>2026-10-03T10:34:31.625Z</updated>
    
    <content type="html"><![CDATA[<h1 id="环境安装"><a href="#环境安装" class="headerlink" title="环境安装"></a>环境安装</h1><p>gcc, g++<br>nvcc编译器</p><p>armadillo数学库</p><h2 id="cmake"><a href="#cmake" class="headerlink" title="cmake"></a>cmake</h2><p>cmake帮你写Makefile, 然后你用make进行编译</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">CMakeLists.txt</span><br><span class="line">        ↓</span><br><span class="line">cmake ..</span><br><span class="line">        ↓</span><br><span class="line">生成 Makefile / CMakeCache.txt / CMakeFiles</span><br><span class="line">        ↓</span><br><span class="line">make 或 cmake --build .</span><br><span class="line">        ↓</span><br><span class="line">调用 g++ 编译 test.cpp</span><br><span class="line">        ↓</span><br><span class="line">生成可执行文件 test</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260602162512.png" alt="510"></p><h3 id="cmake的使用"><a href="#cmake的使用" class="headerlink" title="cmake的使用"></a>cmake的使用</h3><ul><li><strong>第一阶段（构建编译文件）</strong></li></ul><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">cmake ..</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260602162604.png" alt="282"><img src="/images/Pasted%20image%2020260602162634.png" alt="255"></p><ul><li><strong>第二阶段(编译)</strong><br><img src="/images/Pasted%20image%2020260602162707.png" alt="131"><img src="/images/Pasted%20image%2020260602162738.png" alt="334"></li></ul><h3 id="CMakeLists-txt"><a href="#CMakeLists-txt" class="headerlink" title="CMakeLists.txt"></a>CMakeLists.txt</h3><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line"># 表示这个项目最低需要的CMake的版本</span><br><span class="line">cmake_minimum_required(VERSION 3.10)</span><br><span class="line"></span><br><span class="line"># 指定项目名字</span><br><span class="line">project(test_cpp)</span><br><span class="line"></span><br><span class="line"># 使用C++17标准， 相当于在Makefile里面添加 -std=c++17</span><br><span class="line">set(CMAKE_CXX_STANDARD 17)</span><br><span class="line"></span><br><span class="line"># 用test.cpp 生成一个可执行文件test</span><br><span class="line">add_executable(test test.cpp)</span><br></pre></td></tr></table></figure><p>最后生成的Makefile约等于：<code>/usr/bin/g++ -std=c++17 -o test test.cpp</code><br><img src="/images/Pasted%20image%2020260602163104.png" alt="193"><img src="/images/Pasted%20image%2020260602163115.png" alt="209"><img src="/images/Pasted%20image%2020260602163138.png" alt="223"><br><img src="/images/Pasted%20image%2020260602163152.png" alt="234"><img src="/images/Pasted%20image%2020260602163212.png" alt="320"></p><h3 id="cmake真正的好处"><a href="#cmake真正的好处" class="headerlink" title="cmake真正的好处"></a>cmake真正的好处</h3><p><img src="/images/Pasted%20image%2020260602163306.png" alt="276"><img src="/images/Pasted%20image%2020260602163315.png" alt="365"></p><p><img src="/images/Pasted%20image%2020260602163334.png" alt="428"></p><p><code>CMakeLists.txt</code>：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">cmake_minimum_required</span>(VERSION <span class="number">3.10</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">project</span>(test_cpp LANGUAGES CXX)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_executable</span>(<span class="keyword">test</span></span><br><span class="line">main.cpp    </span><br><span class="line">src/add.cpp)</span><br><span class="line"></span><br><span class="line"><span class="keyword">target_include_directories</span>(<span class="keyword">test</span> PRIVATE   </span><br><span class="line"><span class="variable">$&#123;PROJECT_SOURCE_DIR&#125;</span>/<span class="keyword">include</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">target_compile_features</span>(<span class="keyword">test</span> PRIVATE </span><br><span class="line">cxx_std_17)</span><br></pre></td></tr></table></figure><p>这里：</p><figure class="highlight gams"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">add_executable</span>(<span class="params">test ...</span>)</span></span><br></pre></td></tr></table></figure><p><strong>创建一个目标</strong>，叫 <code>test</code>。</p><figure class="highlight gams"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">target_include_directories</span>(<span class="params">test PRIVATE ...</span>)</span></span><br></pre></td></tr></table></figure><p>给 <code>test</code> 这个目标<strong>添加头文件搜索路径</strong>。</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">target_compile_features</span><span class="params">(test PRIVATE cxx_std_17)</span></span></span><br></pre></td></tr></table></figure><p>让 <code>test</code> 用 <strong>C++17 编译</strong>。</p><p>这就是现代 CMake 的核心思想：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line">不要全局乱 <span class="keyword">set</span>尽量围绕 <span class="keyword">target</span> 配置</span><br></pre></td></tr></table></figure><p>也就是围绕：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">add_executable</span><span class="params">()</span></span></span><br><span class="line"><span class="function"><span class="title">add_library</span><span class="params">()</span></span></span><br><span class="line"><span class="function"><span class="title">target_include_directories</span><span class="params">()</span></span></span><br><span class="line"><span class="function"><span class="title">target_link_libraries</span><span class="params">()</span></span></span><br><span class="line"><span class="function"><span class="title">target_compile_options</span><span class="params">()</span></span></span><br><span class="line"><span class="function"><span class="title">target_compile_features</span><span class="params">()</span></span></span><br></pre></td></tr></table></figure><p>这些来写。</p><h3 id="cmake的指令参数"><a href="#cmake的指令参数" class="headerlink" title="cmake的指令参数"></a>cmake的指令参数</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug</span><br><span class="line"></span><br><span class="line"><span class="comment"># -S .</span></span><br><span class="line"><span class="comment"># 指定当前目录为源码目录</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -B build</span></span><br><span class="line"><span class="comment"># 指定构建目录</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -DCMAKE_BUILD_TYPE=Debug</span></span><br><span class="line"><span class="comment"># -D 给CMake定义一个变量</span></span><br><span class="line"><span class="comment"># CMAKE_BUILD_TYPE CMake的内置变量，控制编译模式</span></span><br><span class="line"><span class="comment"># Debug 编译为调试模式</span></span><br><span class="line"><span class="comment"># 保留**调试符号**（gdb / VS 调试可用）</span></span><br><span class="line"><span class="comment"># 不开启优化（`-O0`）</span></span><br><span class="line"><span class="comment"># 方便断点、查崩溃、看变量</span></span><br><span class="line"><span class="comment"># 对应还有：</span></span><br><span class="line"><span class="comment">#`Release`（发布优化）、</span></span><br><span class="line"><span class="comment"># `RelWithDebInfo`（带调试的发布）、</span></span><br><span class="line"><span class="comment"># `MinSizeRel`（最小体积）</span></span><br></pre></td></tr></table></figure><h3 id="项目里的CMake构建速查"><a href="#项目里的CMake构建速查" class="headerlink" title="项目里的CMake构建速查"></a>项目里的CMake构建速查</h3><p>一个项目从 <code>CMakeLists.txt</code> 到可执行文件，大概是这条线：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">include() / find_package()</span><br><span class="line">        ↓</span><br><span class="line">收集源文件</span><br><span class="line">        ↓</span><br><span class="line">add_executable() / add_library()</span><br><span class="line">        ↓</span><br><span class="line">target_include_directories()</span><br><span class="line">target_link_directories()</span><br><span class="line">target_link_libraries()</span><br></pre></td></tr></table></figure><p>根 <code>CMakeLists.txt</code> 通常先做这些事：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">cmake_minimum_required</span>(VERSION <span class="number">3.16</span>)</span><br><span class="line"><span class="keyword">set</span>(CMAKE_EXPORT_COMPILE_COMMANDS <span class="keyword">ON</span>)</span><br><span class="line"><span class="keyword">set</span>(CMAKE_CUDA_COMPILER <span class="string">&quot;/usr/local/cuda/bin/nvcc&quot;</span>)</span><br><span class="line"><span class="keyword">project</span>(llama_infer CXX CUDA)</span><br><span class="line"><span class="keyword">include</span>(cmake/cuda.cmake)</span><br></pre></td></tr></table></figure><p>重点：</p><ul><li><code>CMAKE_EXPORT_COMPILE_COMMANDS ON</code>：生成 <code>compile_commands.json</code>，方便 IDE 跳转和补全。</li><li><code>project(llama_infer CXX CUDA)</code>：启用 C++ 和 CUDA 两套语言支持。</li><li><code>include(cmake/cuda.cmake)</code>：执行 CUDA 探测脚本，比如检查 toolkit、GPU 架构。</li></ul><p>几个容易混的点：</p><ul><li><code>include(xxx.cmake)</code>：执行 CMake 脚本，类似 bash 的 <code>source</code>，不是 C++ 的 <code>#include</code>。</li><li><code>find_package(GTest REQUIRED)</code>：找的是库安装时附带的 CMake 描述文件，不是直接找 <code>.so/.a</code>。</li><li><code>GTest::gtest</code>：CMake target，里面已经打包了头文件路径、库路径和依赖。</li><li><code>PROJECT_SOURCE_DIR</code>：顶层 <code>CMakeLists.txt</code> 所在目录。</li><li><code>REQUIRED</code>：找不到就直接报错停止。</li><li><code>add_library(llama SHARED ...)</code>：<code>llama</code> 是 CMake 目标名，不是最终文件名。<code>SHARED</code> 表示生成动态库，Linux 下 CMake 会自动加 <code>lib</code> 前缀和 <code>.so</code> 后缀，所以产物是 <code>libllama.so</code>。</li></ul><p>顺序上，<code>target_include_directories / target_link_directories / target_link_libraries</code> 必须写在 <code>add_executable/add_library</code> 之后，因为它们要绑定到已经创建好的 target。</p><p>如果有子目录：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">add_library</span>(llama SHARED ...)</span><br><span class="line"><span class="keyword">add_subdirectory</span>(<span class="keyword">test</span>)</span><br><span class="line"><span class="keyword">add_subdirectory</span>(demo)</span><br></pre></td></tr></table></figure><p><code>test/demo</code> 如果要链接 <code>llama</code>，就应该放在 <code>add_library(llama ...)</code> 之后。</p><p>条件编译常用来控制模型支持：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">option</span>(QWEN2_SUPPORT <span class="keyword">OFF</span>)</span><br><span class="line"><span class="keyword">if</span> (QWEN2_SUPPORT)</span><br><span class="line">    <span class="keyword">add_definitions</span>(-DQWEN2_SUPPORT)</span><br><span class="line"><span class="keyword">endif</span>()</span><br></pre></td></tr></table></figure><p>命令行开启：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">cmake .. -DQWEN2_SUPPORT=ON</span><br></pre></td></tr></table></figure><p>用途：不同模型依赖不同，按需打开，避免强制安装暂时不用的库。</p><p>第三方库安装后通常长这样：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">/usr/include/gtest/          # 头文件</span><br><span class="line">/usr/lib/libgtest.a          # 库本体</span><br><span class="line">/usr/lib/cmake/GTest/        # find_package 要找的描述文件</span><br></pre></td></tr></table></figure><p>常用命令：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p build &amp;&amp; <span class="built_in">cd</span> build</span><br><span class="line">cmake ..</span><br><span class="line">make -j$(<span class="built_in">nproc</span>)</span><br><span class="line">make llama</span><br><span class="line">make test_llm</span><br><span class="line">./test_llm --gtest_filter=test_buffer.*</span><br></pre></td></tr></table></figure><p><code>make test_llm</code> 时，如果 <code>test_llm</code> 写了：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">target_link_libraries</span>(test_llm llama)</span><br></pre></td></tr></table></figure><p>CMake 会自动先编 <code>llama</code>，再编 <code>test_llm</code>，不用手动分两步。</p><p>常见目标：</p><table><thead><tr><th>目标</th><th>产物</th></tr></thead><tbody><tr><td><code>llama</code></td><td><code>lib/libllama.so</code></td></tr><tr><td><code>test_llm</code></td><td><code>build/test/test_llm</code></td></tr><tr><td><code>llama_infer</code></td><td><code>build/demo/llama_infer</code></td></tr></tbody></table><p>查看有哪些目标：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">make <span class="built_in">help</span></span><br><span class="line">grep -rn <span class="string">&quot;add_executable\|add_library&quot;</span> CMakeLists.txt <span class="built_in">test</span>/ demo/</span><br></pre></td></tr></table></figure><h3 id="库链接怎么判断"><a href="#库链接怎么判断" class="headerlink" title="库链接怎么判断"></a>库链接怎么判断</h3><p>核心原则：</p><blockquote><p><strong>只要代码用了某个库的头文件，一般就要把对应库写进 <code>target_link_libraries</code>。</strong></p></blockquote><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">target_link_libraries</span>(llama</span><br><span class="line">    sentencepiece</span><br><span class="line">    glog::glog</span><br><span class="line">    gtest</span><br><span class="line">    gtest_main</span><br><span class="line">    pthread</span><br><span class="line">    cudart</span><br><span class="line">    armadillo</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>可以<mark style="background:#fff88f">粗略分三类</mark>：</p><table><thead><tr><th>类型</th><th>例子</th><th>是否需要手动链接</th></tr></thead><tbody><tr><td>编译器默认库</td><td><code>libstdc++</code>、<code>libc</code>、<code>libm</code>、<code>libgcc</code></td><td>通常不用</td></tr><tr><td>系统库但不默认链接</td><td><code>pthread</code>、<code>dl</code>、<code>rt</code></td><td><strong>需要</strong></td></tr><tr><td>第三方库</td><td><code>glog</code>、<code>gtest</code>、<code>sentencepiece</code>、<code>cudart</code>、<code>armadillo</code></td><td><strong>需要</strong></td></tr></tbody></table><p><code>glog::glog</code> 这种带 <code>::</code> 的名字，一般是 CMake target，里面已经带了头文件路径、库路径和依赖。</p><p><code>sentencepiece</code>、<code>cudart</code> 这种裸库名，等价于让链接器去找：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">libsentencepiece.so / libsentencepiece.a</span><br><span class="line">libcudart.so / libcudart.a</span><br></pre></td></tr></table></figure><p>一句话记忆：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">include 解决“编译时能不能看到声明”</span><br><span class="line">target_link_libraries 解决“链接时能不能找到实现”</span><br></pre></td></tr></table></figure><h1 id="GTest单元测试速查"><a href="#GTest单元测试速查" class="headerlink" title="GTest单元测试速查"></a>GTest单元测试速查</h1><p>基本写法：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="built_in">TEST</span>(test_buffer, allocate) &#123;</span><br><span class="line">    <span class="function">Buffer <span class="title">buffer</span><span class="params">(<span class="number">32</span>, alloc)</span></span>;</span><br><span class="line">    <span class="built_in">ASSERT_NE</span>(buffer.<span class="built_in">ptr</span>(), <span class="literal">nullptr</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>TEST(suite, name)</code> 会自动生成测试类，并注册到 gtest 的全局用例列表。<code>RUN_ALL_TESTS()</code> 会统一执行这些测试。</p><p><mark style="background:#d6e4ff">suite 通常写模块名，name 写测试场景</mark>，比如 <code>test_buffer.allocate</code>。</p><p>常用断言：</p><table><thead><tr><th>断言</th><th>含义</th></tr></thead><tbody><tr><td><code>ASSERT_EQ(a, b)</code></td><td><code>a == b</code></td></tr><tr><td><code>ASSERT_NE(a, b)</code></td><td><code>a != b</code></td></tr><tr><td><code>ASSERT_TRUE(cond)</code></td><td>条件为真</td></tr><tr><td><code>ASSERT_FALSE(cond)</code></td><td>条件为假</td></tr><tr><td><code>ASSERT_STREQ(a, b)</code></td><td>C 字符串相等</td></tr></tbody></table><p><mark style="background:#ffd6e7"><code>ASSERT_</code> 失败后立刻终止当前用例，<code>EXPECT_</code> 失败后记录错误但继续执行。</mark></p><p>运行命令：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">make test_llm -j$(<span class="built_in">nproc</span>)</span><br><span class="line"></span><br><span class="line">./build/test/test_llm</span><br><span class="line">./build/test/test_llm --gtest_list_tests</span><br><span class="line">./build/test/test_llm --gtest_filter=test_buffer.*</span><br><span class="line">./build/test/test_llm --gtest_filter=test_buffer.allocate</span><br><span class="line">./build/test/test_llm --gtest_filter=*allocate*</span><br></pre></td></tr></table></figure><p>输出里重点看：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">[ RUN      ] test_buffer.allocate</span><br><span class="line">[       OK ] test_buffer.allocate</span><br><span class="line">[  PASSED  ] 1 test.</span><br></pre></td></tr></table></figure><p>如果失败，会打印断言所在文件行号、期望值和实际值。</p><p>CMake 集成：</p><figure class="highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="keyword">find_package</span>(GTest REQUIRED)</span><br><span class="line"><span class="keyword">target_link_libraries</span>(test_llm GTest::gtest)</span><br></pre></td></tr></table></figure><p><code>test_llm</code> 本质是一个测试可执行文件，里面链接了 <code>libllama.so</code>、gtest、glog 等依赖。</p><h1 id="gdb使用"><a href="#gdb使用" class="headerlink" title="gdb使用"></a>gdb使用</h1><p>gdb是调试工具，需要按照调试模式来编译程序</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 构建</span></span><br><span class="line">cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug</span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译</span></span><br><span class="line">cmake --build build</span><br></pre></td></tr></table></figure><p>重点是我们指定按照Debug模式来编译程序</p><p>之后我们就可以用gdb来进行调试了</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gdb ./build/bin/test</span><br></pre></td></tr></table></figure><ol><li>打断点<ol><li>main<ol><li><code>bread main</code> </li><li><code>b main</code></li></ol></li><li>行号<ol><li><code>break test.cpp:6</code></li><li><code>b test.cpp:6</code></li></ol></li></ol></li><li>开始运行程序<ol><li><code>run</code></li><li><code>r</code></li></ol></li><li>执行<ol><li>单步执行（不进入函数内部）<ol><li><code>next</code></li><li><code>n</code></li></ol></li><li>单步执行（进入函数内部）<ol><li><code>step</code></li><li><code>s</code></li></ol></li><li>执行到下一个断点<ol><li><code>continue</code></li><li><code>c</code></li></ol></li></ol></li><li>查看当前停在哪里<ol><li><code>where</code></li></ol></li><li>查看函数调用栈<ol><li><code>bt</code>  &#x3D; backtrace</li></ol></li><li>查看当前源码附近<ol><li><code>list</code></li><li><code>l</code></li></ol></li><li>打印变量<ol><li>直接打印变量值<ol><li><code>p i</code>    <code>p name</code>   <code>p this-&gt;count</code></li></ol></li><li>格式化打印<ol><li><code>p/x 变量</code> 16进制打印</li><li><code>p/d 变量</code> 10进制打印</li><li>…</li></ol></li><li>打印复杂类型<ol><li>数组&#x2F;连续内存<ol><li><code>p *arr@10</code> 打印arr数组的前10个元素</li></ol></li><li>结构体&#x2F;类<ol><li><code>p 结构体变量</code></li></ol></li><li>漂亮打印(缩进打印结构体)<ol><li><code>set print pretty on</code>  <code>p user</code></li></ol></li></ol></li><li>打印全局变量<ol><li><code>p 变量名</code></li><li><code>p ::count</code> 避免和局部变量重名</li></ol></li><li>打印指针指向内容<ol><li><code>p *ptr</code></li></ol></li><li>连续自动打印(每次停下都显示)<ol><li><code>display 变量</code></li><li>取消自动打印<ol><li><code>undisplay</code></li></ol></li></ol></li><li>查看内存<ol><li><code>x/格式 地址</code><ol><li><code>x/10xw ptr</code>  打印10个32位（word），按16进制(x)</li><li><code>x/20cb ptr</code> 打印20个字节（8位），按照char解析</li></ol></li></ol></li><li>查看所有局部变量<ol><li><code>info locals</code>  一次性打印当前函数所有变量</li></ol></li></ol></li></ol><p><img src="/images/Pasted%20image%2020260602170336.png" alt="222"></p><ol start="8"><li>查看各种界面<ol><li><code>layout src</code> 源码界面<ol><li><img src="/images/Pasted%20image%2020260602170553.png" alt="322"></li></ol></li><li><code>layout asm</code> 汇编界面<ol><li><img src="/images/Pasted%20image%2020260602170707.png" alt="388"></li></ol></li><li><code>layout regs</code> 寄存器界面<ol><li><img src="/images/Pasted%20image%2020260602170757.png" alt="402"></li></ol></li></ol></li></ol><h1 id="CMake"><a href="#CMake" class="headerlink" title="CMake"></a>CMake</h1><p>前面已经知道，cmake是帮助你编写Makefile的，我们可以利用CMake的方法，来自动生成整个项目的Makefile</p><ol><li><code>include(../cmake/cuda.cmake)</code><br>CMakeLists里面的include，是针对同样的cmake来的，这不是引用库，而是<strong>把 <code>cuda.cmake</code> 里的代码原样贴过来执行</strong></li></ol><p><code>.cmake</code> 后缀只是约定，表明”这是个 CMake 脚本”，不是 <code>.so</code> 或 <code>.a</code> 库文件。</p><blockquote><p>这个.cmake说明书的作用：检查cuda工具链是否存在，探测显卡架构，把执行结果，保存到变量里面，后面的代码直接引用<img src="/images/Pasted%20image%2020260627172704.png"></p></blockquote><ol start="2"><li><code>find_package(GTest REQUIRED)</code></li></ol><p>第三方库编译成.so后，安装到系统中(&#x2F;usr&#x2F;lib&#x2F;archxxx&#x2F;)，<br>顺带会装上.cmake描述文件(可以理解为说明书 &#x2F;usr&#x2F;lib&#x2F;archxxx&#x2F;cmake)</p><p>find_package靠.cmake找到库的物理位置<br><img src="/images/Pasted%20image%2020260627171929.png"><br><img src="/images/Pasted%20image%2020260627171941.png"><br>找到库之后，会把检查结果记录到一些变量里面，就是后面的：</p><ul><li>${glog_INCLUDE_DIR}</li><li>${GTest_INCLUDE_DIR}</li></ul><p>所以流程就是：<code>find_package</code> 探测库的位置 → 设置变量 → <code>target_include_directories</code> 用这些变量告诉编译器。</p><ol start="3"><li><p>set(link_ext_lib glog::glog GTest::gtest)<br><img src="/images/Pasted%20image%2020260627172026.png"><br><img src="/images/Pasted%20image%2020260627172039.png"></p></li><li><p>aux_source_directory(目录 变量名)<br><img src="/images/Pasted%20image%2020260627172234.png"></p></li></ol><p><img src="/images/Pasted%20image%2020260627172402.png"></p><ol start="5"><li>add_executable(名字 源文件列表)<br><img src="/images/Pasted%20image%2020260627172833.png"><blockquote><p>注意，这个add_executable还没有正式开始编译，只是注册目标，告诉CMake，要编译什么。</p><p>真正开始构建，是你执行 cmake –build . –target test_llm<br>或者make test_llm</p></blockquote></li></ol><p><img src="/images/Pasted%20image%2020260627173056.png"></p><ol start="6"><li>target_link_libraries(目标 链接库)<br>这个的作用，就是g++里面的-l, 可以链接的库有：</li></ol><ul><li>动态库（.so）（看到.so就只记引用，运行到这一块的时候，才加载）</li><li>静态库（.a）(链接器看到.a就把代码拷贝到可执行文件中)</li></ul><blockquote><p>这里纠正一个认识：<br>给你一个库，so&#x2F;a, 里面肯定是各种方法的实现，但是这个方法叫什么，你肯定在写代码的时候，就需要知道，所以库文件.so&#x2F;.a， 还需要配合头文件，这样你才知道方法叫什么。</p></blockquote><ol start="7"><li><p>target_include_directories(目标 PUBLIC 目录)<br>指定头文件的目录</p></li><li><p>target_link_directories(目标 PUBLIC 目录)<br><img src="/images/Pasted%20image%2020260627174513.png" alt="697"></p></li></ol><p><strong>总结</strong></p><p>用CMakeLists，来利用cmake构建一个项目的make，流程是：<br><img src="/images/Pasted%20image%2020260627174925.png"></p><h1 id="c-20的特性"><a href="#c-20的特性" class="headerlink" title="c++20的特性"></a>c++20的特性</h1><h2 id="命名空间"><a href="#命名空间" class="headerlink" title="命名空间"></a>命名空间</h2><p>我的理解，就是相当于形象的理解成模块归属的机制就行。</p><h2 id="关键字"><a href="#关键字" class="headerlink" title="关键字"></a>关键字</h2><ol><li><p>explicit<br>静止编译器做隐式类型转换，强迫调用者显式写出构造动作，避免编译器帮你自动做类型转换。</p></li><li><p>override<br><code>void* allocate(size_t byte_size) const override;</code><br>用来告诉编译器，我在重写基类虚函数，帮我检查，如果这个不是基类的虚函数，就变成新函数。override的作用是把错误提前到编译阶段。</p></li></ol><p>这个里面的const，是表示常函数，这个函数内部，不会修改这个对象内存的任何成员变量。</p><ol start="3"><li>mutable<br>这个是const的后门，如果const修饰了成员函数，那么内部就不能改变变量，但是有一个例外，就是如果这个成员函数被标记了mutable。那么即便在常函数里面也能修改。</li></ol><h2 id="容器"><a href="#容器" class="headerlink" title="容器"></a>容器</h2><ol><li>map<br>STL标准库里面的有序键值对容器，<code>#include&lt;map&gt;</code></li></ol><h2 id="语法"><a href="#语法" class="headerlink" title="语法"></a>语法</h2><h3 id="static-cast-reinterpret-cast-const-cast"><a href="#static-cast-reinterpret-cast-const-cast" class="headerlink" title="static_cast &#x2F; reinterpret_cast &#x2F; const_cast"></a>static_cast &#x2F; reinterpret_cast &#x2F; const_cast</h3><p>c++风格的强制类型转换， 比（）直接转换更安全。<br>（）转换等于：</p><ul><li><code>static_cast</code> — 相关类型间的转换（只做编译期可检查的转换）</li><li><code>reinterpret_cast</code> — 不相关指针类型间的暴力转换</li><li><code>const_cast</code> — 悄悄去掉 const</li></ul><h3 id="delete-禁止拷贝"><a href="#delete-禁止拷贝" class="headerlink" title="= delete 禁止拷贝"></a><code>= delete</code> 禁止拷贝</h3><p>显式删除某个函数，让编译器禁止调用它。常用于禁止拷贝：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">NoCopyable</span> &#123;</span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line">    <span class="built_in">NoCopyable</span>() = <span class="keyword">default</span>;</span><br><span class="line">    ~<span class="built_in">NoCopyable</span>() = <span class="keyword">default</span>;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">NoCopyable</span>(<span class="type">const</span> NoCopyable&amp;) = <span class="keyword">delete</span>;</span><br><span class="line">    NoCopyable&amp; <span class="keyword">operator</span>=(<span class="type">const</span> NoCopyable&amp;) = <span class="keyword">delete</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>子类继承 <code>NoCopyable</code> 后，普通构造不会受影响：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function">Buffer <span class="title">buf</span><span class="params">(<span class="number">128</span>, alloc)</span></span>;   <span class="comment">// 正常构造</span></span><br></pre></td></tr></table></figure><p>但拷贝构造和拷贝赋值会被编译器拒绝：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function">Buffer <span class="title">buf2</span><span class="params">(buf)</span></span>;     <span class="comment">// 触发拷贝构造，编译报错</span></span><br><span class="line">Buffer buf3 = buf;    <span class="comment">// 也是拷贝构造，编译报错</span></span><br><span class="line">buf2 = buf;           <span class="comment">// 触发拷贝赋值，编译报错</span></span><br></pre></td></tr></table></figure><p><mark style="background:#ffd6e7"><code>= delete</code> 是编译期拦截，不是运行时报错。</mark></p><p>在 <code>Buffer</code> 这种管理大块内存的类里，禁止拷贝可以避免两个对象指向同一块 <code>ptr_</code>，析构时发生 double free。</p><h3 id="enum-和-enum-class"><a href="#enum-和-enum-class" class="headerlink" title="enum 和 enum class"></a><code>enum</code> 和 <code>enum class</code></h3><p>普通 <code>enum</code>：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">StatusCode</span> : <span class="type">uint8_t</span> &#123;</span><br><span class="line">    kSuccess = <span class="number">0</span>,</span><br><span class="line">    kPathNotValid = <span class="number">2</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>枚举值可以直接使用，也容易隐式转成 <code>int</code>：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> code = kSuccess;</span><br></pre></td></tr></table></figure><p><code>enum class</code>：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum class</span> <span class="title class_">DeviceType</span> : <span class="type">uint8_t</span> &#123;</span><br><span class="line">    kDeviceCPU = <span class="number">1</span>,</span><br><span class="line">    kDeviceCUDA = <span class="number">2</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>使用时必须带作用域：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">DeviceType type = DeviceType::kDeviceCUDA;</span><br></pre></td></tr></table></figure><p><mark style="background:#d6e4ff"><code>enum class</code> 更类型安全：不会隐式转成 <code>int</code>，不同枚举类型之间也不能随便比较。</mark></p><p><code>: uint8_t</code> 表示指定底层存储类型，能减少内存占用。默认优先用 <code>enum class</code>，只有需要和 <code>int</code> 高频交互时再考虑普通 <code>enum</code>。</p><h3 id="virtual-xxx-0-纯虚函数"><a href="#virtual-xxx-0-纯虚函数" class="headerlink" title="virtual xxx = 0 纯虚函数"></a><code>virtual xxx = 0</code> 纯虚函数</h3><p><code>= 0</code> 表示纯虚函数：基类只声明接口、不提供实现，子类必须重写；含纯虚函数的类不能直接实例化。</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">virtual</span> base::Status <span class="title">forward</span><span class="params">()</span> </span>= <span class="number">0</span>;</span><br></pre></td></tr></table></figure><h3 id="右值引用和-std-move"><a href="#右值引用和-std-move" class="headerlink" title="右值引用和 std::move"></a>右值引用和 <code>std::move</code></h3><p>左值引用 <code>T&amp;</code>，就是引用一个正常变量，有名字，能取地址。</p><p>右值引用 <code>T&amp;&amp;</code>，可以理解成引用一个临时对象，或者一个后面基本不用的对象。</p><p><mark style="background:#d6e4ff">右值引用的核心作用：告诉编译器，这个对象里面的资源可以被转移走。</mark></p><p><code>std::move</code> 这个名字有点迷惑，它本身不搬数据，也不申请内存，只是做了一次类型转换：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">std::<span class="built_in">move</span>(x) = <span class="built_in">static_cast</span>&lt;T&amp;&amp;&gt;(x)</span><br></pre></td></tr></table></figure><p><mark style="background:#fff88f">std::move 的本质：把一个左值，强行标记成“可以被移动的右值”。</mark></p><p>比如 <code>Tensor</code> 构造函数里面：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">Tensor::<span class="built_in">Tensor</span>(base::DataType data_type, std::vector&lt;<span class="type">int32_t</span>&gt; dims, ...)</span><br><span class="line">    : <span class="built_in">dims_</span>(std::<span class="built_in">move</span>(dims)) &#123;&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>dims</code> 是形参，本身是一个局部变量，所以它其实是左值。</p><p>但是构造函数结束之后，<code>dims</code> 就没用了，所以这里用 <code>std::move(dims)</code>，让成员变量 <code>dims_</code> 直接接管它内部的堆内存。</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">拷贝 vector：重新申请内存，然后一个一个复制元素，O(n)</span><br><span class="line">移动 vector：直接转移 begin/end/cap 三个指针，O(1)</span><br></pre></td></tr></table></figure><p>所以这个写法：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="built_in">Tensor</span>(..., std::vector&lt;<span class="type">int32_t</span>&gt; dims)</span><br><span class="line">    : <span class="built_in">dims_</span>(std::<span class="built_in">move</span>(dims)) &#123;&#125;</span><br></pre></td></tr></table></figure><p>可以理解成：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">调用者传左值：先拷贝到形参 dims，再移动到成员变量 dims_</span><br><span class="line">调用者传右值：直接移动到形参 dims，再移动到成员变量 dims_</span><br></pre></td></tr></table></figure><p><mark style="background:#ffd6e7">注意：被 std::move 之后的对象还能析构，但是不要再依赖它原来的内容。</mark></p><p>几个容易混的点：</p><ul><li><code>dims_(std::move(dims))</code>：这是移动构造，在初始化列表里完成。</li><li><code>dims_ = std::move(dims)</code>：这是移动赋值，成员变量已经存在了。</li><li><code>return t;</code> 返回局部对象时，一般不用写 <code>std::move(t)</code>，编译器会做返回值优化或者自动移动。</li></ul><h3 id="vector-常用方法"><a href="#vector-常用方法" class="headerlink" title="vector 常用方法"></a>vector 常用方法</h3><p><code>std::vector</code> 是动态数组，常用增删改查：</p><table><thead><tr><th>操作</th><th>写法</th><th>说明</th></tr></thead><tbody><tr><td>增</td><td><code>v.push_back(x)</code></td><td>尾部追加</td></tr><tr><td>增</td><td><code>v.emplace_back(args...)</code></td><td>尾部原地构造对象</td></tr><tr><td>增</td><td><code>v.insert(v.begin() + i, x)</code></td><td>指定位置插入</td></tr><tr><td>删</td><td><code>v.pop_back()</code></td><td>删除最后一个元素</td></tr><tr><td>删</td><td><code>v.erase(v.begin() + i)</code></td><td>删除指定位置</td></tr><tr><td>删</td><td><code>v.clear()</code></td><td>清空元素</td></tr><tr><td>改</td><td><code>v[i] = x</code></td><td>修改指定位置</td></tr><tr><td>查</td><td><code>v[i]</code></td><td>直接访问，不做越界检查</td></tr><tr><td>查</td><td><code>v.at(i)</code></td><td>带越界检查</td></tr><tr><td>查</td><td><code>v.front()</code> &#x2F; <code>v.back()</code></td><td>第一个 &#x2F; 最后一个元素</td></tr><tr><td>查</td><td><code>v.size()</code> &#x2F; <code>v.empty()</code></td><td>元素个数 &#x2F; 是否为空</td></tr></tbody></table><p><mark style="background:#ffd6e7"><code>v[i]</code> 越界是未定义行为；<code>v.at(i)</code> 越界会抛异常，更适合防御性代码。</mark></p><p>项目里如果前面已经手动检查过范围，比如 <code>CHECK_LT(idx, weights_.size())</code>，后面再用 <code>weights_.at(idx)</code> 是双保险。</p><h2 id="智能指针"><a href="#智能指针" class="headerlink" title="智能指针"></a>智能指针</h2><ol><li><strong>shared_ptr</strong>, c11引入的智能指针之一，功能：<strong>引用计数自动管理内存，没人用了就自动delete</strong>。<br><img src="/images/Pasted%20image%2020260627084716.png" alt="521"></li></ol><p>创建方法：</p><figure class="highlight asciidoc"><table><tr><td class="code"><pre><span class="line">auto p1 = std::make_shared&lt;Buffer&gt;(128, allocator);</span><br><span class="line"></span><br><span class="line">std::make_shared&lt;类名&gt;(构造函数的参数）</span><br><span class="line"><span class="section">= new 类名（构造函数参数）</span></span><br><span class="line"><span class="section">= new Buffer(128, allocator)</span></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="cuda相关"><a href="#cuda相关" class="headerlink" title="cuda相关"></a>cuda相关</h2><ol><li>异步拷贝，流<br><code>cudaMemcpyAsync</code> 的”异步”是指：<strong>函数调用本身立即返回给 CPU，不等待拷贝实际完成</strong>。对比 <code>cudaMemcpy</code>（同步），CPU 会卡在那行直到拷贝结束。</li></ol><p>至于流内的顺序：同一个流上，任务<strong>依然按挂上去的顺序执行</strong>，后面的任务仍要等前面的异步拷贝完成。异步只是说 CPU 调用那一刻不等。</p><p>所以你这个文件里用 <code>cudaMemcpyAsync</code> 的好处是，CPU 可以快速跑完这段 <code>memcpy</code> 函数，去干别的事（比如准备下一层的输入）。</p><p>cudaDeviceSynchronize, 是cpu阻塞等待GPU所有流的任务完成。只要GPU还在干活，就不返回。</p><ul><li><strong>异步</strong> &#x3D; CPU 不等 GPU 任务完成就继续往下执行。</li><li><strong><code>cudaDeviceSynchronize</code></strong> &#x3D; CPU 死等 GPU 上全部任务干完。</li></ul><hr><h1 id="项目一：cuda自制大模型推理框架"><a href="#项目一：cuda自制大模型推理框架" class="headerlink" title="项目一：cuda自制大模型推理框架"></a>项目一：cuda自制大模型推理框架</h1><h2 id="1-base-基础组件层"><a href="#1-base-基础组件层" class="headerlink" title="1. base&#x2F;基础组件层"></a>1. base&#x2F;基础组件层</h2><p>这边主要三个部分：</p><ul><li>base<ul><li>这个模块主要定义整个推理框架的一些基本类<ul><li>设备类型（枚举）</li><li>数据类型（枚举）</li><li>模型类型（枚举）</li><li>属性类</li><li>状态类</li></ul></li></ul></li><li>buffer<ul><li>这个模块对不同设备的内存空间做了一层抽象封装</li><li>抽象存储空间</li></ul></li><li>alloc<ul><li>这个模块是对<strong>内存分配管理器</strong>的封装</li></ul></li></ul><h3 id="Buffer层，DeviceAlloctor层，RALL动态管理设计"><a href="#Buffer层，DeviceAlloctor层，RALL动态管理设计" class="headerlink" title="Buffer层，DeviceAlloctor层，RALL动态管理设计"></a>Buffer层，DeviceAlloctor层，RALL动态管理设计</h3><p>我的原话问题：</p><blockquote><p>对了，我记得老师说，buffer类这里面有用智能指针+RALL，智能指针我看就一个shared_ptr, 用来对内存进行引用计数，自动进行释放回收。是吗？</p><p>那RALL呢？</p></blockquote><blockquote><p>shared_ptr 本身就是 RAII 的经典实现，这算什么？共享指针本质上就是一个带一个引用计数机制的特殊指针，我看buffer里面，主要就是用来指定DeviceAllocator类型的指针，是用来表示设备操作类对象的指针的。</p><p>buffer类里面，包含的是一块内存的抽象信息（内存大小，内存指针，内存设备，是否是buffer类管理的内存，管理这块内存的DeviceAllocator类对象。）</p><p>而一个指向的具体的设备操作类对象，allocator_，（处理的设备类型，就这一个成员属性）</p><p>所以我们的用共享指针shared_ptr所实现的RALL思想，是什么意思？是指的buffer类包含的对DeviceAllocator类的动态包括关系吗？</p></blockquote><blockquote><p>所以buffer类对象，只是内存块的抽象表示层，中间还有一层DeviceAllocator类的分配层，是吗？</p></blockquote><p>这里可以理解成三层：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">Buffer           # 内存块抽象：大小、指针、设备类型、是否外部内存</span><br><span class="line">    ↓ 委托</span><br><span class="line">DeviceAllocator  # 分配策略：CPU malloc/free 或 CUDA cudaMalloc/cudaFree</span><br><span class="line">    ↓ 调用</span><br><span class="line">OS / CUDA        # 真正执行物理内存分配</span><br></pre></td></tr></table></figure><p><mark style="background:#d6e4ff">Buffer 不直接关心底层是 CPU 还是 GPU，它只保存内存块信息，并把申请&#x2F;释放委托给 allocator_。</mark></p><p>Buffer类 里的核心成员大概是：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="type">size_t</span> byte_size_;</span><br><span class="line"><span class="type">void</span>* ptr_;</span><br><span class="line"><span class="type">bool</span> use_external_;</span><br><span class="line">DeviceType device_type_;</span><br><span class="line">std::shared_ptr&lt;DeviceAllocator&gt; allocator_;</span><br></pre></td></tr></table></figure><ul><li><code>byte_size_ + ptr_</code>：表示这块内存本身。</li><li><code>device_type_</code>：标记内存在 CPU 还是 GPU。</li><li><code>use_external_</code>：如果是外部传进来的指针，Buffer 不负责释放。</li><li><code>allocator_</code>：指向具体分配器对象，负责实际的 <code>allocate/release/memcpy</code>。</li></ul><p><strong>RALL编程思想</strong> &#x3D;<br><strong>Resource Acquisition Is Initialization。</strong><br>核心思想就一条：<strong>把资源的生命周期绑到对象的生命周期上</strong>——<mark style="background:#fff88f">对象构造时获取资源，对象析构时释放资源</mark>。编译器保证析构一定会跑，所以资源永远不会忘了释放。</p><p><mark style="background:#fff88f">RAII 的核心：资源在构造时获取，在析构时释放。</mark></p><p>在这里有两层 RAII：</p><ol><li><code>Buffer</code> 自己管理 <code>ptr_</code> 指向的内存块：构造时申请，析构时释放。<ol><li><strong>buffer层，每个buffer类对象，管理ptr_，是通过构造函数，析构函数，来实现RALL的</strong></li></ol></li><li><code>shared_ptr&lt;DeviceAllocator&gt;</code> 管理分配器对象：多个 Buffer 共享同一个 allocator，最后一个引用消失时自动销毁。<ol><li><strong>DeviceAllocator层，是通过共享指针来动态管理分配器对象，实现RALL的</strong></li></ol></li></ol><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">auto</span> alloc = CPUDeviceAllocatorFactory::<span class="built_in">get_instance</span>();</span><br><span class="line"><span class="function">Buffer <span class="title">buf1</span><span class="params">(<span class="number">32</span>, alloc)</span></span>;</span><br><span class="line"><span class="function">Buffer <span class="title">buf2</span><span class="params">(<span class="number">64</span>, alloc)</span></span>;</span><br></pre></td></tr></table></figure><p>这里 <code>buf1</code>、<code>buf2</code> 都保存了一份 <code>shared_ptr</code>，共同指向同一个 <code>DeviceAllocator</code>。引用计数归零时，分配器对象才会自动释放。</p><p><mark style="background:#ffd6e7">注意：shared_ptr 管的是 allocator 对象的生命周期；Buffer 析构释放的是 ptr_ 指向的那块数据内存。这两个资源不要混在一起。</mark><br><img src="/images/Pasted%20image%2020260628142343.png"></p><h3 id="Cuda流层，kernel模块的RAII封装"><a href="#Cuda流层，kernel模块的RAII封装" class="headerlink" title="Cuda流层，kernel模块的RAII封装"></a>Cuda流层，kernel模块的RAII封装</h3><p>base 层里还有一个 <code>kernel</code> 模块，对 CUDA stream 做了一层轻量封装：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">namespace</span> kernel &#123;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">CudaConfig</span> &#123;</span><br><span class="line">    cudaStream_t stream = <span class="literal">nullptr</span>;</span><br><span class="line"></span><br><span class="line">    ~<span class="built_in">CudaConfig</span>() &#123;</span><br><span class="line">        <span class="keyword">if</span> (stream) &#123;</span><br><span class="line">            <span class="built_in">cudaStreamDestroy</span>(stream);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>CudaConfig</code> 本质上是 <mark style="background:#d6e4ff">CUDA 流的 RAII 包装器</mark>。</p><ul><li><code>stream</code> 保存一条 CUDA 流。</li><li>kernel 函数通过 <code>config-&gt;stream</code> 拿到指定流，在这条流上启动 GPU 算子。</li><li><code>CudaConfig</code> 析构时自动调用 <code>cudaStreamDestroy(stream)</code>，不用外部手动销毁。</li></ul><p><mark style="background:#fff88f">RAII 思想：谁持有资源，谁在析构时负责释放。</mark></p><p>和 <code>Buffer</code> 类似：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">Buffer     管 ptr_ 指向的内存块</span><br><span class="line">CudaConfig 管 cudaStream_t 指向的 CUDA 流</span><br></pre></td></tr></table></figure><p>区别只是资源类型不同，一个是内存，一个是 CUDA 执行流。</p><p><img src="/images/Pasted%20image%2020260628144842.png"></p><h2 id="2-tensor-张量层"><a href="#2-tensor-张量层" class="headerlink" title="2. tensor&#x2F;张量层"></a>2. tensor&#x2F;张量层</h2><p>这一层就是在buffer层的基础上，定义tensor层，上下层之间也是共享指针来动态绑定来实现RALL</p><p><img src="/images/Pasted%20image%2020260630145735.png"></p><p>这边主要仔细看了一下构造函数，张量转移函数。</p><p><strong>构造函数</strong></p><p><strong>张量转移函数</strong><br><img src="/images/Pasted%20image%2020260630155727.png"></p><p>这里来解释一下，</p><ul><li>先判断当前tensor的设备类型，必须是cpu内存</li><li>之后<ul><li>获取内存大小</li><li>工厂函数获取GPU的内存分配器（实际干活的人）<ul><li>工厂函数获取gpu内存分配器</li><li><blockquote><p><img src="/images/Pasted%20image%2020260630160056.png"></p></blockquote></li><li>可以看到，工厂函数类对象，维护者一个静态的真实的gpu内存分配器指针，instance这个共享内存指针，永远指向一个gpu内存分配器。</li><li>之后所有申请gpu内存分配器，就是用auto cu_alloc来指向这个gpu内存分配器的对象，增加了引用计数。这也是RALL的思想。</li></ul></li><li>分配器开辟gpu内存</li><li>分配器拷贝内存</li><li>绑定该内存</li></ul></li></ul><h2 id="3-op-算子层"><a href="#3-op-算子层" class="headerlink" title="3. op&#x2F;算子层"></a>3. op&#x2F;算子层</h2><h3 id="层的概念复习"><a href="#层的概念复习" class="headerlink" title="层的概念复习"></a>层的概念复习</h3><p>前面我们已经构造好了整个框架层base&#x2F;， 里面实现了：</p><ul><li>内存的分配，内存的抽象<br>之后，我们又在tensor张量层，来基于buffer层，实现了张量。</li></ul><p>现在我们要开始实现深度学习里面最经常用到的组件：层，他表示一种运算，输入张量，得到另一个张量。<br><img src="/images/Pasted%20image%2020260701161755.png" alt="256"><br><img src="/images/Pasted%20image%2020260701161819.png" alt="267"></p><p>所以我们就要实现一个层 类，来抽象这个针对张量的运算过程的概念。</p><h3 id="算子层op-的总体架构"><a href="#算子层op-的总体架构" class="headerlink" title="算子层op&#x2F;的总体架构"></a>算子层op&#x2F;的总体架构</h3><p>所以op&#x2F;算子层，这里主要有这几个部分：</p><ul><li>layer，层接口层<ul><li>主要作为前端实现，确定好：<ul><li>输入张量组</li><li>输出张量组</li><li>权重张量组</li></ul></li></ul></li><li>后端接口层：kernel_interface<ul><li>利用函数指针，重定向具体的后端实现</li></ul></li><li>op， 后端实现forward(), 拿数据进行具体的计算<ul><li><code>op/add.h .cpp</code></li><li><code>op/linear.h .cpp</code></li><li><code>op/matmul.h .cpp</code> </li><li>各种具体的层的运算实现</li></ul></li></ul><p>所以，至此，我们的大模型推理框架的整个结构是：<br><img src="/images/Pasted%20image%2020260701173218.png"></p><p>根据前面，我们得知了</p><h4 id="层组件层-（y-f-x-的前端结构）"><a href="#层组件层-（y-f-x-的前端结构）" class="headerlink" title="层组件层 （y &#x3D; f(x) 的前端结构）"></a>层组件层 （y &#x3D; f(x) 的前端结构）</h4><p>首先是层组件层：</p><ul><li><mark style="background:#fff88f">BaseLayer类（层的固有属性）</mark><ul><li><p><img src="/images/Pasted%20image%2020260701191035.png"></p></li><li><p><mark style="background:#fff88f"> Layer类，（层的固有属性+输入输出张量）</mark></p><ul><li><img src="/images/Pasted%20image%2020260701191253.png"></li></ul></li><li><p><mark style="background:#fff88f">LayerParam类（层的固有属性+输入输出张量+权重张量）</mark></p><ul><li><img src="/images/Pasted%20image%2020260701191337.png"></li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260701194256.png" alt="394"></p><p>上面这个是第一部分，主要定义了一个<strong>层组件层</strong>，这样一个<strong>中间层</strong>，实现了张量-张量的层运算的抽象过程。</p><blockquote><p>内部的具体逻辑没有，只有形式, 就相当于实现了y &#x3D; f(x) ，这样一个前端框架，但是f(x)的具体后端实现并没有定义，只是通过<strong>forward()这个基类的虚函数</strong>，在**子类（算子）中实现。</p></blockquote><p>在这个<strong>中间层的基础上</strong>，我们：</p><ul><li>在<strong>上方构建算子层（实现各个算子）</strong><ul><li>各个算子的后端实现，forward()，依赖后端算子内核接口层返回具体的内核方法<ul><li>cpu后端内核实现</li><li>cuda后端内核实现</li></ul></li></ul></li><li>在<strong>下方依赖于Tensor层</strong></li></ul><h4 id="后端内核接口层（kernel-interface）"><a href="#后端内核接口层（kernel-interface）" class="headerlink" title="后端内核接口层（kernel_interface）"></a>后端内核接口层（kernel_interface）</h4><p>这里就是前端连接后端的接口层。通过这一层的方法，自动判断使用哪个后端内核。</p><ol><li><p>定义各种算子的<strong>后端内核方法</strong>的<strong>函数指针接口</strong><br><img src="/images/Pasted%20image%2020260701192154.png"></p></li><li><p>提供<strong>返回函数指针的方法</strong>， 提供自动索引后端内核<br><img src="/images/Pasted%20image%2020260701192245.png"></p></li><li><p><strong>具体返回的后端内核</strong>，涉及了<code>cpu/add_kernel.h</code> 和 <code>cuda/add_kernel.cuh</code><br><img src="/images/Pasted%20image%2020260701192448.png" alt="385"></p></li></ol><h4 id="具体算子层-f-的真正实现"><a href="#具体算子层-f-的真正实现" class="headerlink" title="具体算子层( f()的真正实现 )"></a>具体算子层( f()的真正实现 )</h4><p>这一层，就是基于前面两个层，创建各个算子的抽象类，继承Layer层，让不同算子类都是继承的层的概念。并重写各自的forward()</p><p>因此，通过调用每个算子类对象的forward()，即可调用真正的后端算子内核实现。</p><p>下面以add_kernel为例：</p><ol><li><strong>定义加法算子：</strong></li></ol><p> <img src="/images/Pasted%20image%2020260701192851.png" alt="339"></p><ol start="2"><li><strong>算子方法实现：</strong><ol><li><strong>设定算子的输入输出</strong><br><img src="/images/Pasted%20image%2020260701193108.png" alt="335"></li><li><strong>实现前向传播forward</strong></li></ol></li></ol><p><img src="/images/Pasted%20image%2020260701193056.png" alt="474"></p><p>可以看到，f()的实现里面，调用了后端内核接口层的get_add_kernel方法，来返回一个根据设备类型而选择的后端内核</p><p><code>kernel::get_add_kernel(device_type_)</code> &#x3D; <code>AddKernel</code></p><p><code> kernel::get_add_kernel(device_type_)(input1, input2, output, cuda_config_ ? cuda_config_-&gt;stream : nullptr);</code><br>&#x3D; <code>Addkernel(......)</code></p><p>从而在forward()里面实现调用对应的内核接口</p><h5 id="cpu端内核"><a href="#cpu端内核" class="headerlink" title="cpu端内核"></a>cpu端内核</h5><ul><li>首先进行一系列对输入输出的检查</li><li>计算得出结果（利用armadillo库，这个是c++的线性代数库）</li></ul><p><img src="/images/Pasted%20image%2020260701193654.png"></p><h5 id="cuda端内核"><a href="#cuda端内核" class="headerlink" title="cuda端内核"></a>cuda端内核</h5><ul><li>对输入输出进行检查</li><li>实现后端主函数，调用核函数</li><li>实现后端核函数</li></ul><p><img src="/images/Pasted%20image%2020260701193914.png"></p><h4 id="算子输入规划策略"><a href="#算子输入规划策略" class="headerlink" title="算子输入规划策略"></a>算子输入规划策略</h4><blockquote><p>我想知道，我们的这个cuda算子，我们的算子cu实现是肯定不知道，处理的输入数据是多大的，</p><p>我看我目前的add算子，是固定设置好了block_size的大小，然后grid_size根据实际输入的数据大小来定的，但是我目前的add算子是朴素实现，一个thread处理一个元素输出。 </p><p>但是如果后续优化的话，比如SGEMM，可能就会有thread_tile, warp_tile等优化策略，这就导致，我们一个thread处理多个元素。 那么这样的话，如果我写这样的优化算子的话，那么还是一样的吗？</p><p>比如，我默认想好使用这种优化手段，所以一个thread对应x个元素，所以设置一个block_size &#x3D; 256这样，然后我再根据输入的大小来动态计算grid_size吗？</p></blockquote><p><mark style="background:#ff4d4f">对，核心逻辑完全一样</mark>，只是 grid_size 的计算公式里要多除一个系数。</p><p><strong>朴素实现</strong>（你现在的 add）：1 thread &#x3D; 1 element</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">grid_size = <span class="built_in">ceil</span>(size / block_size);</span><br><span class="line"><span class="comment">// kernel 里：</span></span><br><span class="line"><span class="type">int</span> tid = blockIdx.x * blockDim.x + threadIdx.x;</span><br><span class="line"><span class="keyword">if</span> (tid &lt; size) out[tid] = in1[tid] + in2[tid];</span><br></pre></td></tr></table></figure><p><strong>线程平铺优化</strong>（比如 thread_tile &#x3D; 4）：1 thread &#x3D; 4 elements</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="type">size_t</span> block_size  = <span class="number">256</span>;             <span class="comment">// 还是固定</span></span><br><span class="line"><span class="type">size_t</span> thread_tile = <span class="number">4</span>;               <span class="comment">// 每个线程处理 4 个元素</span></span><br><span class="line"><span class="type">size_t</span> grid_size   = (size + block_size * thread_tile - <span class="number">1</span>) </span><br><span class="line">                   / (block_size * thread_tile);</span><br><span class="line"></span><br><span class="line"><span class="comment">// kernel 里：</span></span><br><span class="line"><span class="type">int</span> tid   = blockIdx.x * blockDim.x + threadIdx.x;</span><br><span class="line"><span class="type">int</span> start = tid * thread_tile;        <span class="comment">// 这个线程负责的起始位置</span></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> unroll</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> k = <span class="number">0</span>; k &lt; thread_tile; k++) &#123;</span><br><span class="line">    <span class="type">int</span> idx = start + k;</span><br><span class="line">    <span class="keyword">if</span> (idx &lt; size) out[idx] = in1[idx] + in2[idx];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>SGEMM 那种更复杂的</strong>（有 warp_tile），也是同样模式，只是分得更细：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">total_tiles</span> = (M * N) / (thread_tile * warp_tile)</span><br><span class="line"><span class="attr">grid_size</span>   = ceil(total_tiles / block_size)</span><br></pre></td></tr></table></figure><p>本质上就是：</p><blockquote><p><strong>block_size 永远是固定的（硬件友好常量），grid_size 永远是根据数据量动态算出来的，只是”一个 thread block 能做多少工作”的计算公式变了。</strong></p></blockquote><p>你用固定 block_size + 动态 grid_size 这个模式是对的，不管怎么优化都适用。</p><hr><blockquote><p><mark style="background:#ff4d4f">那block_size，要怎么确定呢？</mark>是根据SM上的寄存器数量以及共享内存的大小，考虑到一个thread负责多少个数据，来推导出来的？一般是多少？</p></blockquote><p>对，block_size 的确定就是这几个因素互相制约：</p><p><strong>核心公式（任何一个 SM 上）：</strong></p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">MaxWarps × WarpSize <span class="operator">=</span> min(</span><br><span class="line">    MaxThreadsPerBlock,</span><br><span class="line">    RegisterFileSize <span class="symbol">/</span> RegistersPerThread,</span><br><span class="line">    SharedMemPerBlock <span class="symbol">/</span> SharedMemPerThread</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p><strong>具体推演，拿常见的架构举例：</strong></p><blockquote><p>一个寄存器，4 字节。GPU 寄存器是 32 位的，正好装一个 <code>float</code> 或 <code>int32_t</code></p></blockquote><table><thead><tr><th>架构</th><th>SM 寄存器数</th><th>最大线程&#x2F;block</th><th>共享内存&#x2F;SM</th></tr></thead><tbody><tr><td>V100 (SM70)</td><td>65536</td><td>1024</td><td>96KB</td></tr><tr><td>A100 (SM80)</td><td>65536</td><td>1024</td><td>164KB</td></tr></tbody></table><p>假设你的 kernel 每个线程用 32 个寄存器：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">RegistersPerBlock</span>  = max(<span class="number">65536</span> / <span class="number">32</span>, <span class="number">1024</span>) = <span class="number">1024</span> threads  // 寄存器够用，卡在硬件上限</span><br></pre></td></tr></table></figure><p>假设用了 48KB 共享内存&#x2F;block：</p><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">SharedMemPerBlock</span>  = max(<span class="number">164</span>KB / <span class="number">48</span>KB, ...) = <span class="number">3</span> blocks/SM   // 共享内存卡住并行</span><br></pre></td></tr></table></figure><p><strong>实际情况中最常见的 block_size：</strong></p><table><thead><tr><th>block_size</th><th>原因</th></tr></thead><tbody><tr><td><strong>256</strong></td><td>最常用，8 个 warp，寄存器&#x2F;共享内存均衡，适用大多数 kernel</td></tr><tr><td><strong>128</strong></td><td>寄存器用量高的时候（每个 thread 算得多），让更多 block 并行</td></tr><tr><td><strong>512</strong></td><td>轻量 kernel（寄存器少，共享内存少），最大化 occupancy</td></tr><tr><td><strong>1024</strong></td><td>几乎没寄存器压力的极轻 kernel，跑满硬件限制</td></tr></tbody></table><p><strong>你的情况：</strong> 你用的是 512，对朴素 add 完全合适——一个 thread 只做一次 load + add + store，寄存器压力极小，共享内存也没有，往大了设没问题。</p><p><strong>建议：</strong> 一般 128 或 256 作为默认值就够，具体调哪个可以在 kernel 测试的时候调一下看看哪个快，这东西理论推导只能给出范围，最终还是跑 profile 说话。</p><hr><h3 id="3-2-大模型算子内容介绍"><a href="#3-2-大模型算子内容介绍" class="headerlink" title="3.2 大模型算子内容介绍"></a>3.2 大模型算子内容介绍</h3><p>先复习一下我们的decoder-only的transoformer的整个架构</p><p><img src="/images/b0e134623d6da1f889d335edef9df60c.jpg"></p><p>下面逐个看一下各个算子的实现，看看各个算子实际的输入输出是什么</p><h4 id="encode-分词编码器层"><a href="#encode-分词编码器层" class="headerlink" title="encode 分词编码器层"></a>encode 分词编码器层</h4><p>这个就是一个单纯的无参翻译层算子，</p><ul><li>给一个词串，他就翻译成一个token id的数组</li><li>给一个词，就翻译成单个token</li></ul><p>所以 <code>encode(&quot;hello world&quot;)</code> 的过程是：</p><figure class="highlight applescript"><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;hello world&quot;</span></span><br><span class="line">  → BPE 算法切分: [<span class="string">&quot;Ġhello&quot;</span>, <span class="string">&quot;Ġworld&quot;</span>]     (子词拆分)</span><br><span class="line">  → 查词袋:        Ġhello → token <span class="built_in">id</span> = <span class="number">2</span></span><br><span class="line">                   Ġworld → token <span class="built_in">id</span> = <span class="number">3</span></span><br><span class="line">  → 输出:                                 [<span class="number">2</span>, <span class="number">3</span>]</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="embedding-词嵌入层-（有参）"><a href="#embedding-词嵌入层-（有参）" class="headerlink" title="embedding 词嵌入层 （有参）"></a>embedding 词嵌入层 （有参）</h4><p>我们一个token，经过encode层之后，获得的是这个token在vocab词袋中的索引id, 这样我们其实可以得到的是最原始的一个编码：one-hot编码</p><p>比如token id &#x3D; 2， 用one-hot编码表示就是<br>[0,0,1,0,0,……………………………………………………………………………..,0]<br>但是用这样一个vocab_size( &#x3D; 36000)的维度的编码太长了，没必要，所以就需要embedding层，来把他映射压缩到一个较低维度的编码比如4096，这就是词嵌入</p><p>由于one-hot编码的特性，本质上就变成了，对embedding层的权重矩阵的查表。</p><p><img src="/images/img_v3_0213l_01510ec1-1484-4151-8994-aa2a9a83b00g.jpg"></p><blockquote><p>这个权重矩阵，也就是embedding层的参数，是训练出来的，模型在训练过程中，自己学会压缩token向量的维度</p></blockquote><figure class="highlight livecodeserver"><table><tr><td class="code"><pre><span class="line"><span class="literal">one</span>-hot: <span class="keyword">token</span> <span class="string">&quot;Ġhello&quot;</span> → [<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,...,<span class="number">0</span>]   <span class="number">32000</span> 维稀疏向量（只有一个 <span class="number">1</span>）</span><br><span class="line">embedding: 同一个 <span class="keyword">token</span> → [<span class="number">0.04</span>, <span class="number">-0.02</span>, <span class="number">0.01</span>, ..., <span class="number">0.03</span>]  <span class="number">4096</span> 维稠密向量</span><br><span class="line"></span><br><span class="line">关系: <span class="literal">one</span>-hot @ W = W[id] = 稠密向量</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>本质就是<strong>降维 + 语义化</strong>：</p><ul><li>one-hot 只是编号，<code>id=2</code> 和 <code>id=3</code> 在向量空间里等距，没有意义</li><li>embedding 让”语义相近的词在向量空间里也近” — “hello” 和 “hi” 的 embedding 向量很接近，这是训练过程中自己学出来的</li></ul><p>所以 embedding 层 &#x3D; 一个 <code>vocab_size × dim</code> 的大矩阵，训练帮每个 token 学到了一个”好的”低维表示。</p><p>所以embedding层输出的一个token向量，是一个dim维度的向量，这个dim维度的向量空间V，就是一个<mark style="background:#fff88f">语义空间</mark>。</p><p>我们这就实现了<mark style="background:#fff88f">从一个token词，到语义空间的投射</mark>。</p><h4 id="rmsnorm-归一化层（有参）"><a href="#rmsnorm-归一化层（有参）" class="headerlink" title="rmsnorm 归一化层（有参）"></a>rmsnorm 归一化层（有参）</h4><p>这里先捋一下这部分的实际大模型用的一个顺序</p><figure class="highlight stata"><table><tr><td class="code"><pre><span class="line">embedding 输出 (1×4096)</span><br><span class="line">  │</span><br><span class="line">  ▼  <span class="keyword">input</span></span><br><span class="line">┌─────────────────────────────────────────────────────────────────┐</span><br><span class="line">│  Decoder Block #i (重复 layer_num 次)                              │</span><br><span class="line">│                                                                   │</span><br><span class="line">│  ① attention_rms:                                                 │</span><br><span class="line">│     <span class="keyword">input</span> ──► RMSNorm ──► rmsnorm_out                             │</span><br><span class="line">│                              │                                    │</span><br><span class="line">│  ② attention_qkv:            │                                    │</span><br><span class="line">│                    ┌─ W_Q ─ Q ─ RoPE ─► kQuery                   │</span><br><span class="line">│     rmsnorm_out ──┼─ W_K ─ K ─ RoPE ─► KV Cache[i][pos]         │</span><br><span class="line">│                    └─ W_V ─ V ────────► KV Cache[i][pos]         │</span><br><span class="line">│                                                                   │</span><br><span class="line">│  ③ attention_mha:                                                 │</span><br><span class="line">│     <span class="built_in">Q</span>(kQuery) + K,V(KV Cache) ──► MHA ──► W_O ──► kAttnOutput   │</span><br><span class="line">│                                                                   │</span><br><span class="line">│  ④ feed_forward:                                                  │</span><br><span class="line">│     <span class="keyword">input</span> + kAttnOutput ────────────────────────────► <span class="keyword">input</span>&#x27;      │</span><br><span class="line">│     <span class="keyword">input</span>&#x27; ──► RMSNorm ──► ffn_norm_out                           │</span><br><span class="line">│                              │                                    │</span><br><span class="line">│                    ┌─ W1(gate) ─┐                                 │</span><br><span class="line">│     ffn_norm_out ─┼─ W3(up)   ─┼─► SwiGLU ──► W2(down) ──► <span class="keyword">out</span>  │</span><br><span class="line">│                    └───────────┘                                   │</span><br><span class="line">│     <span class="keyword">input</span>&#x27; + <span class="keyword">out</span> ─────────────────────────────────► <span class="keyword">input</span> (下一层)│</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">cls_logits:</span><br><span class="line">  <span class="keyword">input</span> ──► RMSNorm ──► lm_head ──► logits (1×vocab_size)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以，decode阶段，当来了一个新的token 从embedding层出来，是一个新的token 向量，</p><p>然后进入decoder block的<mark style="background:#ff4d4f">第一个算子层是RMSNorm层，他是一个有参层</mark></p><p>这个RMSNorm层，属于层归一化LayerNorm的优化版本</p><p>原来LayerNorm，对每个token向量的处理:<br><img src="/images/Pasted%20image%2020260702101001.png" alt="209"><img src="/images/Pasted%20image%2020260702101104.png" alt="357"></p><p>所以RMSNorm对一个向量的公式是：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">RMSNorm</span><span class="params">(x_i)</span></span> = (x_i / <span class="built_in">RMS</span>(<span class="attribute">x</span>)) × γ_i</span><br><span class="line"></span><br><span class="line">其中:</span><br><span class="line">  <span class="built_in">RMS</span>(<span class="attribute">x</span>) = <span class="built_in">sqrt</span>( <span class="number">1</span>/dim × Σ(x_i²) )    ← 纯计算，不需要参数</span><br><span class="line">  γ      = <span class="selector-attr">[γ₁, γ₂, ..., γ_4096]</span>       ← 可训练参数，训练学到</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>γ 做的事就是”这个维度重要就放大，不重要就缩小”</strong></p><figure class="highlight subunit"><table><tr><td class="code"><pre><span class="line">归一化后:  [0.01, <span class="string">-0</span>.5, 0.3, ..., <span class="string">-0</span>.02]   每个值都在 [<span class="string">-1</span>,1] 附近</span><br><span class="line">  × γ:     [0.01×1.2, <span class="string">-0</span>.5×0.8, 0.3×1.5, ..., <span class="string">-0</span>.02×0.9]</span><br><span class="line">  = 输出:  [0.012, <span class="string">-0</span>.4, 0.45, ..., <span class="string">-0</span>.018]</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>让模型自己学会”哪些维度该放大、哪些该缩小”。</strong></p><p>归一化把所有维度压到同一个量级，这很粗暴 — 有些语义特征可能需要更大的变化幅度。γ 就是给模型留下自由度：</p><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">假设 dim <span class="number">3</span> 编码<span class="string">&quot;名词/动词&quot;</span>信息  →  训练后 γ₃ <span class="operator">=</span> <span class="number">1.5</span>（放大，让这个特征更突出）</span><br><span class="line">假设 dim <span class="number">7</span> 编码<span class="string">&quot;标点符号&quot;</span>信息  →  训练后 γ₇ <span class="operator">=</span> <span class="number">0.2</span>（缩小，不太重要）</span><br></pre></td></tr></table></figure><p>如果只有归一化没有 γ，相当于强制 γ &#x3D; [1, 1, 1, …, 1]，模型就失去了<strong>调节每个维度表达力的能力</strong>，这也是为什么纯归一化（无参）之后表现会显著下降。</p><p>简单说：归一化负责稳定数值（别让梯度炸了），γ 负责保留表达能力（别把有用信息也压没了）</p><p>但是，这里的RMSNorm，虽然也有微调每个token的维度的能力，但是本质上是为了稳定，而真正的表达，是依赖后续的matmul算子的权重张量来负责的。</p><figure class="highlight autoit"><table><tr><td class="code"><pre><span class="line">RMSNorm × γ     (<span class="keyword">dim</span> 个参数)    → 纯缩放，调节数值范围</span><br><span class="line">W_Q 矩阵        (<span class="keyword">dim</span> × <span class="keyword">dim</span> 个参数) → 真正的语义变换</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>γ 就是个”量级微调”</strong>，给归一化留一点点灵活性。真正把向量从”被归一化压平的”变成”包含丰富语义的”，靠的是后面 <code>dim × dim</code> 的权重矩阵。</p><p>换个角度看：RMSNorm 在每个 block 里出现两次，后面永远紧跟着至少一个 <code>dim × dim</code> 的矩阵乘法。如果你觉得某个维度被归一化后变弱了，那后面那个矩阵完全可以学出对应的补偿 —— 但 γ 可以让这个补偿更直接、训练更稳定。</p><p>实际上有研究试过把 γ 固定为全 1（DynamicsNorm），性能差不太多。所以你说得对，它的调节能力确实有限，但这正是它的设计意图 — 别抢权重矩阵的活</p><h4 id="Matmul-矩阵乘算子层（有参）"><a href="#Matmul-矩阵乘算子层（有参）" class="headerlink" title="Matmul 矩阵乘算子层（有参）"></a>Matmul 矩阵乘算子层（有参）</h4><p>经过embedding后得到的是token向量[4096]， 之后会复制3份，分别经过，Wq,Wk,Wv 矩阵乘的算子层，来计算得到我们的Q向量[4096]，K向量[1024]，V向量[1024]。</p><p><img src="/images/9d445fd58c26dd5a3f08a987bb566f06.jpg" alt="260"><img src="/images/Pasted%20image%2020260718143926.png" alt="345"></p><p><mark style="background:#affad1">那么既然embedding已经把token投影到语义空间了，qkv的投影矩阵的投影，又是什么意义呢？</mark></p><p><img src="/images/Pasted%20image%2020260718144300.png"></p><hr><p><strong>以上解释了，我们矩阵乘Wq,Wk,Wv三个算子层的作用，下面来解释为什么</strong></p><ul><li>q是4096</li><li>k是1024</li><li>v是1024</li></ul><blockquote><p>我的问题现在集中在，<strong>为什么token向量是4096，q仍然是4096，而kv则是1024</strong>，就是单纯的为了省kvcache吗？kv压缩了维度，不会对模型理解语义效果有什么影响吗</p></blockquote><p><img src="/images/Pasted%20image%2020260719093036.png"></p><p><strong>所以原因就是k,v的各个头之间是冗余的，所以可以压缩，就投影到1024，而不是4096.</strong></p><hr><p>下面来解释多头的概念。</p><p>所谓多头，其实就是去切分q,k,v的这么长的向量，不同分块sub就是一个头。</p><p>以llama3为例，</p><ul><li>k,v  1024维度，切分8个头，每个头128维</li><li>q，4096维度，切分32个头，每个头128维</li></ul><p><img src="/images/Pasted%20image%2020260719093608.png"></p><blockquote><p>在标准的transformer论文里面，W_Q&#x2F;W_K&#x2F;W_V 确实属于 Attention 的一部分。但<strong>在这个项目里被拆开了</strong>。</p></blockquote><hr><h5 id="多头方案"><a href="#多头方案" class="headerlink" title="多头方案"></a>多头方案</h5><p>在接着往下之前，先分析几个<strong>多头方案</strong></p><p>本质上解决的就是<strong>两个头数不同的向量怎么做点积</strong>：</p><p><mark style="background:#d3f8b6">MHA</mark><br>q,k,v都是一个维度，4096维度，切分32个头，每个头一一对应<br><img src="/images/img_v3_0213o_89ca8fba-41ce-44b4-a5d7-8b87b7f0f9fg.jpg" alt="547"></p><p><mark style="background:#affad1">MQA</mark><br>特点是，k,v向量，被极端压缩，就压成一个头的大小<br><img src="/images/img_v3_0213o_5c109d55-804b-41ee-9f7b-be554fb41d6g.jpg" alt="574"></p><p><mark style="background:#affad1">GQA</mark><br>Q的几个头一组，共享KV的一个头<br><img src="/images/img_v3_0213o_32ca7599-3790-43b7-86fd-b377cf469fbg.jpg" alt="570"></p><p>以上仅是分组的策略，本质上都是如何并行计算两个向量之间的相关程度。</p><p><mark style="background:#fff88f">效果对比</mark><br><img src="/images/Pasted%20image%2020260719095456.png"></p><hr><h4 id="RoPE-旋转位置编码层"><a href="#RoPE-旋转位置编码层" class="headerlink" title="RoPE 旋转位置编码层"></a>RoPE 旋转位置编码层</h4><p>在进入mha层之前，会对，<strong>q,k两个向量进行位置编码</strong>，</p><p>这里位置编码的处理，不是整个向量处理，而是<strong>一次针对一个头128维来处理</strong>的。</p><p><img src="/images/Pasted%20image%2020260719100127.png" alt="451"></p><p><img src="/images/Pasted%20image%2020260719100910.png" alt="609"></p><h5 id="总结"><a href="#总结" class="headerlink" title="总结"></a><mark style="background:#affad1">总结</mark></h5><p>所以，一个token向量，投影成QKV向量后，</p><ul><li>kv向量加入kvcache，作为这个token的键值缓存，用来被其他token向量计算相关性。</li><li>Q向量则用来和历史token的KV向量计算相关性。</li></ul><p> 所以，因为<strong>每个向量的Q，K经过RoPE</strong>，这样，这个token和另一个token的QK进行<strong>计算相关性</strong>的时候，就带入了相对位置关系，是吧</p><p>这里的位置关系，需要说明一下，</p><p>我们对一个token向量的Q，K向量做位置编码，<strong>本质上是给这些向量一个数学上的绝对位置坐标</strong></p><p>这样，该token的q和其他token的k计算的时候，<strong>得到的相对位置关系，其实就是先后顺序关系</strong><br><img src="/images/Pasted%20image%2020260719104759.png"></p><p>所以，加入RoPE，是为注意力分数的相关性里面，增加了一层位置信息，这样注意力分数的相关性包含：</p><ul><li>位置先后顺序</li><li>语义相关性</li></ul><p><img src="/images/Pasted%20image%2020260719105000.png"></p><p>注意，在具体实现中，会提前将旋转运算用到的系数，计算好存在 sin cos cache里面，<strong>相当于提前计算好了整个上下文窗口的位置编码的旋转系数</strong>。</p><p><img src="/images/Pasted%20image%2020260720211704.png"></p><hr><h4 id="mha多头注意力算子层"><a href="#mha多头注意力算子层" class="headerlink" title="mha多头注意力算子层"></a>mha多头注意力算子层</h4><p>mha这个算子层的实现，他就是，当前token的kv向量直接加入kvcache， q向量和历史token的k向量做相关性计算</p><p><img src="/images/Pasted%20image%2020260719103015.png"></p><p>可以看到，我们<strong>前面的多头策略，本质上就是定义两个不同维度的向量的点积运算</strong>。</p><p>所以实际进行相关分数计算的时候，是发生在里面的<strong>头和头</strong>之间的</p><p><img src="/images/Pasted%20image%2020260719103527.png"></p><p><img src="/images/Pasted%20image%2020260719103856.png" alt="406"></p><p><strong>一个q和所有的kcache进行注意力分数计算</strong></p><p><img src="/images/img_v3_0213o_7da7076f-c02c-42ea-999a-af113f5a31ag.jpg"></p><p>在输出注意力输出的时候，因为v是1024维度，只有8个头，所以，和k一样，GQA共享，也就是说，每组的q</p><p>所以，i个kcache, i个vcache, 一个q 4096, 32个头，</p><p><strong>最终产生的v_q, 就是4096维度的</strong>（32个头都有分数，分组加权vcache）</p><h6 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h6><p><strong>最终还有一个W_O的矩阵乘算子层，把多头信息投影到语义空间</strong><br><img src="/images/img_v3_0213o_c47507d5-7c42-4d59-8891-5cb4ad63b2fg.jpg"></p><h4 id="残差连接层-add"><a href="#残差连接层-add" class="headerlink" title="残差连接层 add"></a>残差连接层 add</h4><p><img src="/images/img_v3_0213o_090cbb72-b0eb-48cb-8d09-d85eadcb699g.jpg"></p><p>至此，就是decoder block的前半段。</p><h4 id="（补充）门控GLU"><a href="#（补充）门控GLU" class="headerlink" title="（补充）门控GLU"></a>（补充）门控GLU</h4><p><img src="/images/Pasted%20image%2020260720093900.png" alt="424"></p><p>假设现在有一个输入x, 经过两个线性层linear算子，但是其中一个算子后面增加了sigmoid，</p><p><img src="/images/Pasted%20image%2020260720094011.png" alt="153"><img src="/images/Pasted%20image%2020260720094043.png" alt="286"></p><p><img src="/images/Pasted%20image%2020260720094119.png" alt="107"><br>因此B这条linear层算子，因为加上了sigmoid，就会变成一个门控单元，之后和A的linear层进行逐元素相乘即可</p><p>这就是我们最原始的GLU的思想</p><p><img src="/images/Pasted%20image%2020260720094304.png"></p><h4 id="SwiGLU"><a href="#SwiGLU" class="headerlink" title="SwiGLU"></a>SwiGLU</h4><p>而我们的decoder-block的后半段，是FFN部分，这里不是单纯的两个linear层，</p><p><img src="/images/Pasted%20image%2020260720094654.png" alt="412"></p><p><img src="/images/Pasted%20image%2020260720095147.png" alt="483"></p><p>所以，可以看到，他这里把sigmoid函数换成了 x*sigmoid(x), 这样的好处是：</p><ul><li>原来激活函数用sigmoid，只能通过、抑制，不能放大信息</li><li>swish函数：-0.3——-+∞， 可以实现关闭，衰减，保留，放大，改变符号。</li></ul><p>所以，kuipa里面的SwiGLU算子层，就是把升维的向量，和门控的向量，做一个运算的无参层。</p><p>真正的升维的都是矩阵乘matmul算子<br><img src="/images/Pasted%20image%2020260720100128.png" alt="253"></p><p>经过SwiGLU之后，得到升维的token向量。<br><img src="/images/Pasted%20image%2020260720100431.png" alt="543"><br><img src="/images/Pasted%20image%2020260720100556.png"></p><h5 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h5><p>之后这一块也有残差连接<br><img src="/images/Pasted%20image%2020260720101015.png" alt="466"></p><p><img src="/images/Pasted%20image%2020260720100935.png" alt="497"></p><h4 id="分层算子-cls-layer（本质上matmul算子）-有参"><a href="#分层算子-cls-layer（本质上matmul算子）-有参" class="headerlink" title="分层算子 cls_layer（本质上matmul算子） (有参)"></a>分层算子 cls_layer（本质上matmul算子） (有参)</h4><p>至此，当我们经过了多个decoder block， 得到最后的一个输出token向量 4096，经过最后一个RMSNorm，归一化到0-1之间，之后，就可以开始解码了。我们解码，用的就是embedding的逆用。</p><p>这里就需要再次深入的认识embedding这个算子层了。</p><p>在一开始，embedding算子层的作用是把token_id -&gt; token向量，本质上就是token id的one hot编码对embedding的巨大的权重矩阵（vocab_size x dim）的查表。</p><p>在最后，我们用输出向量4096，也反向去embedding的权重矩阵里面去查表，得到最后的token id. 这就是我们的输出token的id。</p><blockquote><p>所以，embedding的权重矩阵，不是无意义的编码，而是每个token的语义坐标<br><img src="/images/Pasted%20image%2020260720102131.png"></p></blockquote><p><img src="/images/Pasted%20image%2020260720102223.png"></p><p>所以<strong>末端的</strong>cls_layer<strong>层，通过语义坐标来查找匹配对应的tokenid， 并不是完全一致。而是匹配看看那个向量最接近</strong>。</p><p><img src="/images/Pasted%20image%2020260720102735.png"></p><p>所以，cls_layer层算子，本质上就是一个matmul算子，因为里面指定了特定的权重矩阵，所以，才单独封装一下。</p><p><img src="/images/Pasted%20image%2020260720103058.png"></p><p><img src="/images/Pasted%20image%2020260720103316.png"></p><p><mark style="background:#ff4d4f">cls_layer计算得到可能输出tokenid的整个分数表，下面就是解码策略了</mark>， 到底如何选择一个实际的token? 也就是<strong>sampler，采样器</strong>。</p><ul><li><p><strong>argmax</strong> — 直接选分数最大的那个 token</p><ul><li>没做 temperature、top-k、top-p，就是贪心选</li></ul></li><li><p><img src="/images/Pasted%20image%2020260720103745.png" alt="613"></p></li><li><p>Temperature — 控制”创造力”（<strong>放大距离值</strong>）</p><ul><li><img src="/images/Pasted%20image%2020260720104356.png" alt="538"></li></ul></li><li><p>Softmax — 转成概率（<strong>距离转成概率</strong>）</p><ul><li><img src="/images/Pasted%20image%2020260720104456.png" alt="542"></li></ul></li><li><p>Top-K &#x2F; Top-P — 砍掉烂选择</p><ul><li><img src="/images/Pasted%20image%2020260720104525.png" alt="398"></li></ul></li><li><p>随机采样</p><ul><li><img src="/images/Pasted%20image%2020260720104550.png" alt="408"></li></ul></li></ul><h4 id="推理框架部分结束"><a href="#推理框架部分结束" class="headerlink" title="推理框架部分结束"></a>推理框架部分结束</h4><p><img src="/images/Pasted%20image%2020260720104711.png"></p><h5 id="（旧）matmul算子（有权重层）"><a href="#（旧）matmul算子（有权重层）" class="headerlink" title="（旧）matmul算子（有权重层）"></a><del>（旧）matmul算子（有权重层）</del></h5><p><img src="/images/5028bee2a1327396c026e7e6f0a04352.jpg" alt="430"></p><p><img src="/images/Pasted%20image%2020260702104115.png" alt="346"><img src="/images/Pasted%20image%2020260702104128.png" alt="234"></p><p>可以看到，这个矩阵乘的算子层，也是有权重参数的。设置权重张量，是直接用父类的方法，设置偏执张量，就要由子类提供。<br><img src="/images/Pasted%20image%2020260702104246.png" alt="470"></p><p><img src="/images/Pasted%20image%2020260702104429.png" alt="513"><br>这里可以看到，矩阵乘算子层的输入张量1个，输出张量1个，权重张量1个。</p><p>后端实现，指向两个内核，一个是量化的内核<br><img src="/images/Pasted%20image%2020260702104557.png"></p><h5 id="（旧）MHA多头注意力算子层（无权重）"><a href="#（旧）MHA多头注意力算子层（无权重）" class="headerlink" title="（旧）MHA多头注意力算子层（无权重）"></a><del>（旧）MHA多头注意力算子层（无权重）</del></h5><p><img src="/images/Pasted%20image%2020260702111927.png" alt="474"><img src="/images/Pasted%20image%2020260702111942.png" alt="471"></p><p>可以看到，多头注意力层，它本质上，是一个无权重的层算子。</p><p>下面看一下他的输入输出张量<br><img src="/images/Pasted%20image%2020260702113253.png" alt="507"></p><p>可以看到这个层算子，他的输入设置了5个张量，输出一个张量。</p><p>但是很奇怪的是，在推理过程中，我们就是一个张量进（[B,T&#x3D;1,d]），一个张量出[B,T&#x3D;1,d]</p><p>但是这里，针对一个q, 我要计算得到一个注意力输出v_q,需要：</p><ul><li>q查询输入张量</li><li>K cache （所有历史的k键张量）</li><li>V cache   (所有历史的v值张量)</li><li>q 与 所有K cache的注意力分数张量<ul><li>这个注意力分数score，理论上是属于临时变量，用来计算注意力输出的。但是每次都需要重新分配显存，通讯延时太慢，所以，选择和kv cache的内存空间一样，预先分配，用空间换时间</li></ul></li><li><strong>还有一个未用到</strong><br><img src="/images/Pasted%20image%2020260702113714.png"></li></ul><p>下面看一下这个<strong>多头注意力层的算子的前向传播</strong></p><p><img src="/images/Pasted%20image%2020260702114618.png"></p><p>所以，当我们设计架构的时候，我们可以<mark style="background:#fff88f">通过张量，把并行的需求，封装透传给底层的并行核心单元。中间的框架层，不需要处理这些并行的需求</mark><br><img src="/images/Pasted%20image%2020260702114609.png" alt="578"></p><h2 id="4-model-模型组装层"><a href="#4-model-模型组装层" class="headerlink" title="4. model&#x2F;模型组装层"></a>4. model&#x2F;模型组装层</h2><p>这一层，就是用上面的各个算子层，来具体组合出一个大模型的类。</p><p>我们先来介绍一下，我们下载下来的模型文件有哪些内容：</p><h3 id="huggingface下载的模型文件"><a href="#huggingface下载的模型文件" class="headerlink" title="huggingface下载的模型文件"></a>huggingface下载的模型文件</h3><p>包含：</p><figure class="highlight tap"><table><tr><td class="code"><pre><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji       <span class="number"> 726 </span>May<span class="number"> 17 </span>19:23 config.json</span><br><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji       <span class="number"> 239 </span>May<span class="number"> 17 </span>19:23 generation_config.json</span><br><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji<span class="number"> 1503300328 </span>May<span class="number"> 17 </span>19:30 model.safetensors</span><br><span class="line"></span><br><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji      <span class="number"> 9732 </span>May<span class="number"> 17 </span>19:23 tokenizer_config.json</span><br><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji  <span class="number"> 11422654 </span>May<span class="number"> 17 </span>19:23 tokenizer.json</span><br><span class="line">-rw-rw-r--<span class="number"> 1 </span>liangji liangji   <span class="number"> 2776833 </span>May<span class="number"> 17 </span>19:23 vocab.json</span><br></pre></td></tr></table></figure><ul><li><p><mark style="background:#ff4d4f">config.json</mark></p><ul><li>模型的架构参数（层数，模型维数，头数，<strong>词表大小</strong>）</li></ul></li><li><p><mark style="background:#ff4d4f">generation_config.json</mark></p><ul><li>采样器的超参数</li></ul></li><li><p><mark style="background:#ff4d4f">model.safetensors</mark></p><ul><li>模型权重，有他的特定格式。</li></ul></li><li><p><mark style="background:#ff4d4f">tokenizer.json</mark></p><ul><li><strong>词袋</strong></li><li><mark style="background:#fff88f">传给第三方的分词器类就行, 主要用这个</mark></li><li><img src="/images/Pasted%20image%2020260720192503.png"></li></ul></li><li><p><mark style="background:#ff4d4f">tokenizer_config.json</mark></p><ul><li><strong>存特殊 token 配置</strong></li><li>分词器元信息（bos, eos token &#x3D; ？）</li><li>这个里面的tokenizer_class &#x3D; Qwen2Tokenizer， 暗示BPE类分词器</li><li>用来选择</li><li><img src="/images/Pasted%20image%2020260720192420.png"></li></ul></li><li><p>vocab.json</p><ul><li>词表映射文件</li><li><img src="/images/Pasted%20image%2020260720192649.png" alt="588"></li></ul></li></ul><blockquote><p>算法类型（BPE&#x2F;SentencePiece）是”约定俗成”的 — Llama2 用 SentencePiece，Llama3&#x2F;Qwen 用 BPE，你下载前就知道。</p></blockquote><h4 id="模型文件的格式处理"><a href="#模型文件的格式处理" class="headerlink" title="模型文件的格式处理"></a>模型文件的格式处理</h4><ul><li>架构文件，config.json<ul><li>这个没多少，主要定义层数，模型维度这些，看一下，读取直接用就行</li></ul></li><li>权重文件，model.safetensors<ul><li>这个很多，而且是safetensors的特定格式，我们自己的推理框架cpp无法直接使用，需要格式转换</li></ul></li></ul><p><strong>转换方法</strong>就是，</p><ol><li>先用torch库，自己写一个model.py，里面实现我们的整个模型的类。</li><li>export.py中，先读取config.json，import model, 实例化我们的模型类<ol><li>在这一步里面，我们把官方的权重变量名，和我们自己的权重变量名对应上</li><li><img src="/images/Pasted%20image%2020260720185154.png" alt="588"></li></ol></li><li>按照我们想要的存储结构，导出模型的各个权重张量。<ol><li><img src="/images/Pasted%20image%2020260720185241.png" alt="563"></li></ol></li></ol><p>最终，<mark style="background:#fff88f">会产生一个新的model.bin</mark>, 里面包含</p><ul><li>模型的架构配置</li><li>模型的权重<br>之后，我们在自己的cpp的推理框架中，就可以直接用mmap+地址偏移，来直接读取</li></ul><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">┌────────────────────────────────┐  offset 0</span><br><span class="line">│  dim          (int32, 4B)      │</span><br><span class="line">│  hidden_dim   (int32, 4B)      │</span><br><span class="line">│  layer_num    (int32, 4B)      │  ← ModelConfig header</span><br><span class="line">│  head_num     (int32, 4B)      │    7 × int32 = 28 bytes</span><br><span class="line">│  kv_head_num  (int32, 4B)      │</span><br><span class="line">│  vocab_size   (int32, 4B)      │</span><br><span class="line">│  seq_len      (int32, 4B)      │</span><br><span class="line">├────────────────────────────────┤  offset 28</span><br><span class="line">│  tok_embeddings.weight         │  ① embedding</span><br><span class="line">│  [vocab_size, dim]             │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].attention_norm      │</span><br><span class="line">│  [dim]                         │</span><br><span class="line">│  layers[1].attention_norm      │  ② attention_norm × layer_num</span><br><span class="line">│  [dim]                         │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].attention_norm    │</span><br><span class="line">│  [dim]                         │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].attention.wq        │</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">│  layers[1].attention.wq        │  ③ Wq × layer_num</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].attention.wq      │</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].attention.wk        │</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">│  layers[1].attention.wk        │  ④ Wk × layer_num</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].attention.wk      │</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].attention.wv        │</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">│  layers[1].attention.wv        │  ⑤ Wv × layer_num</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].attention.wv      │</span><br><span class="line">│  [kv_dim, dim]                 │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].attention.wo        │</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">│  layers[1].attention.wo        │  ⑥ Wo × layer_num</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].attention.wo      │</span><br><span class="line">│  [dim, dim]                    │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].ffn_norm            │</span><br><span class="line">│  [dim]                         │</span><br><span class="line">│  layers[1].ffn_norm            │  ⑦ ffn_norm × layer_num</span><br><span class="line">│  [dim]                         │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].ffn_norm          │</span><br><span class="line">│  [dim]                         │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].feed_forward.w1     │</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">│  layers[1].feed_forward.w1     │  ⑧ W1 (gate) × layer_num</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].feed_forward.w1   │</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].feed_forward.w2     │</span><br><span class="line">│  [dim, hidden_dim]             │</span><br><span class="line">│  layers[1].feed_forward.w2     │  ⑨ W2 (down) × layer_num</span><br><span class="line">│  [dim, hidden_dim]             │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].feed_forward.w2   │</span><br><span class="line">│  [dim, hidden_dim]             │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  layers[0].feed_forward.w3     │</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">│  layers[1].feed_forward.w3     │  ⑩ W3 (up) × layer_num</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">│  ...                           │</span><br><span class="line">│  layers[N-1].feed_forward.w3   │</span><br><span class="line">│  [hidden_dim, dim]             │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  model.norm.weight             │  ⑪ final_norm</span><br><span class="line">│  [dim]                         │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  freqs_cos                     │  ⑫ cos/sin 缓存</span><br><span class="line">│  [seq_len, head_size/2]        │     (freqs_cos + freqs_sin)</span><br><span class="line">│  freqs_sin                     │</span><br><span class="line">│  [seq_len, head_size/2]        │</span><br><span class="line">├────────────────────────────────┤</span><br><span class="line">│  model.output.weight           │  ⑬ CLS (仅 vocab_size &gt; 0)</span><br><span class="line">│  [vocab_size, dim]             │     共享 embedding 时不写</span><br><span class="line">└────────────────────────────────┘</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="model的基类"><a href="#model的基类" class="headerlink" title="model的基类"></a>model的基类</h3><ul><li>model (基类)<ul><li>模型的基础信息<ul><li>推理设备</li><li>模型名称</li></ul></li><li>模型的入口，出口<ul><li>入口：分词器层算子</li><li>出口：采样层算子</li></ul></li><li>模型的配置信息（config.json）<ul><li>模型的配置架构：config.json</li><li>权重文件</li></ul></li><li>分词器<ul><li>词袋（tokenizer.json）</li><li>分词算法</li></ul></li><li>提前申请的缓冲区<ul><li>各层的输入输出张量缓冲区</li><li>kvcache</li></ul></li><li>量化标志</li></ul></li></ul><p><img src="/images/Snipaste_2026-07-13_17-04-00.png"></p><h2 id="5-demo构建"><a href="#5-demo构建" class="headerlink" title="5. demo构建"></a>5. demo构建</h2><h2 id="6-profile性能分析"><a href="#6-profile性能分析" class="headerlink" title="6. profile性能分析"></a>6. profile性能分析</h2><h3 id="性能指标"><a href="#性能指标" class="headerlink" title="性能指标"></a>性能指标</h3><p>首先，先给出我们分析一个推理框架系统的性能指标：</p><ul><li><strong>端到端性能指标</strong>（测结果，快不快）<ul><li>TTFT</li><li>TPOT</li><li>端到端延时</li><li>吞吐量</li><li>显存占用</li></ul></li><li><strong>阶段性指标</strong>（找原因，为什么不快）<br>  - prefill<br>      - 各层的耗时<br>  - decode<br>      - 各层的耗时<ul><li><strong>Nsight System</strong><ul><li>各个kernel的耗时，找出瓶颈kernel</li></ul></li><li><strong>Nsight Compute</strong><ul><li>单独分析kernel性能</li></ul></li></ul></li></ul><hr><h3 id="指标术语"><a href="#指标术语" class="headerlink" title="指标术语"></a>指标术语</h3><ul><li><strong>benchmark</strong> &#x3D; 基准测试<ul><li><strong>在固定条件下，正式测性能，统计端到端指标</strong></li><li>Benchmark 用来得到一个可重复、可对比的性能结果。</li><li><mark style="background:#fff88f">Benchmark 告诉你“快不快”</mark></li></ul></li><li><strong>profile</strong> &#x3D; 性能剖析<ul><li><mark style="background:#fff88f">时间到底花在哪里，为什么慢。</mark></li><li><img src="/images/Pasted%20image%2020260722233352.png" alt="128"></li><li><mark style="background:#fff88f">profile又分成两类</mark>：<ul><li><strong>Nsight Systems</strong>：看整个 CPU、GPU、kernel、内存传输的时间线</li><li><strong>Nsight Compute</strong>：深入分析某个 kernel 的访存、计算、occupancy、stall</li></ul></li></ul></li></ul><p>所以，</p><p><strong>Benchmark：测结果</strong><br><strong>Profile：找原因</strong></p><ul><li><p><strong>warmup</strong> &#x3D; 预热运行</p><ul><li>正式计时前，先运行几次，但不记录结果</li><li><img src="/images/Pasted%20image%2020260722233650.png" alt="241"></li><li><img src="/images/Pasted%20image%2020260722234018.png" alt="452"></li></ul></li><li><p><strong>NVTX</strong> &#x3D; 给 Nsight 时间线添加标签的工具</p><ul><li><img src="/images/Pasted%20image%2020260722233831.png" alt="309"></li><li><img src="/images/Pasted%20image%2020260722233918.png" alt="356"></li></ul></li></ul><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">Warmup</span><br><span class="line">  ↓</span><br><span class="line">先跑几次，让环境稳定</span><br><span class="line"></span><br><span class="line">Benchmark</span><br><span class="line">  ↓</span><br><span class="line">测 TTFT、TPOT、tokens/s 等总体指标</span><br><span class="line"><span class="built_in"></span></span><br><span class="line"><span class="built_in">Profile</span></span><br><span class="line"><span class="built_in"></span>  ↓</span><br><span class="line">分析时间花在哪里、为什么慢</span><br><span class="line"></span><br><span class="line">NVTX</span><br><span class="line">  ↓</span><br><span class="line">给<span class="built_in"> Profile </span>时间线添加模块名称</span><br></pre></td></tr></table></figure><p><strong>ITL：每个 token 之间的间隔统计</strong><br><img src="/images/Pasted%20image%2020260723000404.png" alt="397"></p><hr><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><h4 id="1-nsight-system的打标基础工具，nvtx3"><a href="#1-nsight-system的打标基础工具，nvtx3" class="headerlink" title="1. nsight system的打标基础工具，nvtx3"></a>1. nsight system的打标基础工具，nvtx3</h4><p>他先搓了一点基础工具<br><img src="/images/Pasted%20image%2020260722234550.png" alt="357"><br>他自己写了一个打标的类，构造一个对象，然后把自己注册到nvtxRangePushEx里面来打标</p><p><img src="/images/Pasted%20image%2020260722234658.png"><br><img src="/images/Pasted%20image%2020260722234749.png" alt="665"></p><p><img src="/images/Pasted%20image%2020260722234801.png"></p><p><img src="/images/Pasted%20image%2020260722234838.png"></p><p>回到我们的代码</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 开启 ENABLE_NVTX 时：</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">NvtxRange</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">NvtxRange</span><span class="params">(<span class="type">const</span> std::string&amp; name)</span> </span>&#123;</span><br><span class="line">    nvtxEventAttributes_t attr = &#123;&#125;;          <span class="comment">// 构造属性</span></span><br><span class="line">    attr.version = NVTX_VERSION;</span><br><span class="line">    attr.size = NVTX_EVENT_ATTRIBUTES_STRUCT_SIZE;</span><br><span class="line">    attr.messageType = NVTX_MESSAGE_TYPE_ASCII;</span><br><span class="line">    attr.message.ascii = name.<span class="built_in">c_str</span>();        <span class="comment">// 把名字填进去</span></span><br><span class="line">    <span class="built_in">nvtxRangePushEx</span>(&amp;attr);                   <span class="comment">// 告诉驱动：开始一段叫 name 的 range</span></span><br><span class="line">  &#125;</span><br><span class="line">  ~<span class="built_in">NvtxRange</span>() &#123; <span class="built_in">nvtxRangePop</span>(); &#125;            <span class="comment">// 告诉驱动：这个 range 结束</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 没开 ENABLE_NVTX 时：</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">NvtxRange</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">NvtxRange</span><span class="params">(<span class="type">const</span> <span class="type">char</span>*)</span> </span>&#123;&#125;           <span class="comment">// 空构造，什么都不做</span></span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// NVTX_RANGE(&quot;xxx&quot;) → do &#123; (void)(&quot;xxx&quot;); &#125; while(0)   // 完全编译掉</span></span><br></pre></td></tr></table></figure><p><strong>所以整体的数据流是</strong>：</p><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="title">NVTX_RANGE</span><span class="params">(<span class="string">&quot;prefill&quot;</span>)</span></span>   <span class="comment">// 你的代码</span></span><br><span class="line">  → <span class="built_in">nvtxRangePushEx</span>()   <span class="comment">// nvtx3 库</span></span><br><span class="line">    → cupti 接口         <span class="comment">// CUDA 驱动内部</span></span><br><span class="line">      → trace buffer     <span class="comment">// GPU 驱动环形缓存</span></span><br><span class="line">        → nsys profile   <span class="comment">// 采集时写入 .nsys-rep 文件</span></span><br><span class="line">          → Nsight Systems GUI  <span class="comment">// Windows 上打开看到 timeline</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260722235256.png"></p><p>下面介绍他是如何具体实现nvtx打标，然后在nsight system上看到打标的<img src="/images/Pasted%20image%2020260723163420.png"></p><p>这边定义了两个宏，用来构造创建nvtx标签的实例。</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> NVTX_RANGE(name)       profile::NvtxRange _nvtx_ ## __LINE__(name, 0)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> NVTX_RANGE_C(name, c)  profile::NvtxRange _nvtx_ ## __LINE__(name, c)</span></span><br></pre></td></tr></table></figure><p>后面在构造函数中，往cuda驱动的缓冲区打标签，析构函数中弹出标签。<br><img src="/images/Pasted%20image%2020260723163535.png" alt="341"><br>所以，计时开始就是push的cpu时间，弹出时间就是计时结束。</p><p><strong>以rmsnorm为例</strong></p><p><img src="/images/Pasted%20image%2020260723163656.png" alt="445"><br>在llama3.cpp中，attention_rms这个调用算子的过程，NVTX_RANGE_C开始打标。这个实例对象的生命周期，和最近的{}绑定，结束后pop析构，计时结束。因此就可以<strong>实现对一个生命周期的追踪</strong>。</p><p>我们实际上要统计的，就是从NVTX_RANGE_C开始，到rmsnorm_layer-&gt;forward结束。</p><p>但是这个forward一直追踪到调用核函数<br><img src="/images/Pasted%20image%2020260723163916.png" alt="412"></p><p>cpu调用核函数之后，没有阻塞等待直接返回。所以我们的nvtx打的标签记录的时间，本质上是cpu时间，因此，我们的kernel的实际运行时间，是会有可能右端超出标签弹出时间的。<br><img src="/images/Pasted%20image%2020260723164115.png"></p><h4 id="2-gpu计时器（cudaEvent）"><a href="#2-gpu计时器（cudaEvent）" class="headerlink" title="2. gpu计时器（cudaEvent）"></a>2. gpu计时器（cudaEvent）</h4><p>定义了cudaTimer的类</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// CUDA Event RAII wrapper — start/stop a named timer on a stream</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CudaTimer</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">CudaTimer</span><span class="params">(<span class="type">const</span> std::string&amp; name)</span></span>;</span><br><span class="line">  ~<span class="built_in">CudaTimer</span>();</span><br><span class="line"></span><br><span class="line">  <span class="built_in">CudaTimer</span>(<span class="type">const</span> CudaTimer&amp;) = <span class="keyword">delete</span>;</span><br><span class="line">  CudaTimer&amp; <span class="keyword">operator</span>=(<span class="type">const</span> CudaTimer&amp;) = <span class="keyword">delete</span>;</span><br><span class="line">  <span class="built_in">CudaTimer</span>(CudaTimer&amp;&amp; other) <span class="keyword">noexcept</span>;</span><br><span class="line">  CudaTimer&amp; <span class="keyword">operator</span>=(CudaTimer&amp;&amp; other) <span class="keyword">noexcept</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Record start on the given stream (nullptr = default stream / synchronize).</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_start</span><span class="params">(cudaStream_t stream = <span class="literal">nullptr</span>)</span></span>;</span><br><span class="line">  <span class="comment">// Record stop on the given stream.</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_stop</span><span class="params">(cudaStream_t stream = <span class="literal">nullptr</span>)</span></span>;</span><br><span class="line">  <span class="comment">// Synchronize and return elapsed time in milliseconds.</span></span><br><span class="line">  <span class="function"><span class="type">float</span> <span class="title">elapsed_ms</span><span class="params">(<span class="type">bool</span> sync = <span class="literal">true</span>)</span></span>;</span><br><span class="line">  <span class="comment">// Return name for reporting.</span></span><br><span class="line">  <span class="function"><span class="type">const</span> std::string&amp; <span class="title">name</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> name_; &#125;</span><br><span class="line">  <span class="comment">// Check if events are created and valid.</span></span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">is_valid</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> created_; &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">  std::string name_;</span><br><span class="line">  cudaEvent_t start_event_ = <span class="literal">nullptr</span>;</span><br><span class="line">  cudaEvent_t stop_event_ = <span class="literal">nullptr</span>;</span><br><span class="line">  <span class="type">bool</span> started_ = <span class="literal">false</span>;</span><br><span class="line">  <span class="type">bool</span> stopped_ = <span class="literal">false</span>;</span><br><span class="line">  <span class="type">bool</span> created_ = <span class="literal">false</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260722235602.png"></p><p><img src="/images/Pasted%20image%2020260722235820.png"></p><h4 id="3-定义汇总结果-结构体"><a href="#3-定义汇总结果-结构体" class="headerlink" title="3. 定义汇总结果 结构体"></a>3. 定义汇总结果 结构体</h4><p><img src="/images/Pasted%20image%2020260722235951.png"></p><h4 id="4-Profiler-类（统计总管）"><a href="#4-Profiler-类（统计总管）" class="headerlink" title="4. Profiler 类（统计总管）"></a>4. Profiler 类（统计总管）</h4><p><img src="/images/Pasted%20image%2020260723110736.png"></p><p>profiler相当于MemoryAllocator，用来创建&#x2F;读取我们的timer类对象</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Profiler</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="built_in">Profiler</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 显存统计</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_memory_before_model</span><span class="params">()</span></span>;<span class="comment">//这3个是对cudaMemGetInfo的包装</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_memory_after_model</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_peak_memory</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="function"><span class="type">size_t</span> <span class="title">memory_before_mb</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> mem_before_mb_; &#125;</span><br><span class="line">  <span class="function"><span class="type">size_t</span> <span class="title">memory_after_mb</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> mem_after_mb_; &#125;</span><br><span class="line">  <span class="function"><span class="type">size_t</span> <span class="title">memory_peak_mb</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> mem_peak_mb_; &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//CPU时间计时打点</span></span><br><span class="line">  <span class="comment">// ---- CPU-wall-clock timing (for phases where GPU isn&#x27;t involved) ----</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">set_cpu_start</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="function"><span class="type">float</span> <span class="title">cpu_elapsed_ms</span><span class="params">()</span> <span class="type">const</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//GPU时间计时打点</span></span><br><span class="line">  <span class="comment">// ---- GPU cudaEvent timers ----</span></span><br><span class="line">  <span class="comment">// Create/get a named timer that lives for the profiler&#x27;s lifetime.</span></span><br><span class="line">  <span class="function">CudaTimer* <span class="title">get_timer</span><span class="params">(<span class="type">const</span> std::string&amp; name)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ---- Layer profiling ----</span></span><br><span class="line">  <span class="comment">// Record a layer module timing. Called by the model during forward() when</span></span><br><span class="line">  <span class="comment">// layer profiling is enabled.</span></span><br><span class="line">  <span class="comment">// 一个层跑完了，记录数据</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">add_layer_record</span><span class="params">(<span class="type">const</span> std::string&amp; <span class="keyword">module</span>, <span class="type">const</span> std::string&amp; stage,</span></span></span><br><span class="line"><span class="params"><span class="function">                        <span class="type">int32_t</span> layer_idx, <span class="type">float</span> elapsed_ms)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ---- Stage profiling (lightweight, no per-layer sync) ----</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">set_stage</span><span class="params">(<span class="type">const</span> std::string&amp; stage_name)</span></span>;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">record_stage_time</span><span class="params">(<span class="type">const</span> std::string&amp; stage_name, <span class="type">float</span> elapsed_ms)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ---- Benchmark result generation ----</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">add_run</span><span class="params">(<span class="type">const</span> RunRecord&amp; run)</span></span>;<span class="comment">//一次完整的推理结束，记录数据</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">  <span class="comment">//计算平均的benchmark</span></span><br><span class="line">  <span class="function">BenchmarkResult <span class="title">compute_result</span><span class="params">(<span class="type">const</span> std::string&amp; model_path,</span></span></span><br><span class="line"><span class="params"><span class="function">                                 <span class="type">int32_t</span> prompt_tokens, <span class="type">int32_t</span> max_new_tokens,</span></span></span><br><span class="line"><span class="params"><span class="function">                                 <span class="type">int32_t</span> warmup_iterations, <span class="type">int32_t</span> repeat_iterations,</span></span></span><br><span class="line"><span class="params"><span class="function">                                 <span class="type">bool</span> greedy, <span class="type">int32_t</span> seed)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 查询相关信息</span></span><br><span class="line">  <span class="function"><span class="type">const</span> std::vector&lt;RunRecord&gt;&amp; <span class="title">runs</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> runs_; &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">clear_runs</span><span class="params">()</span> </span>&#123; runs_.<span class="built_in">clear</span>(); &#125;</span><br><span class="line">  <span class="function"><span class="type">const</span> std::vector&lt;LayerModuleRecord&gt;&amp; <span class="title">layer_records</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> layer_records_; &#125;</span><br><span class="line">  <span class="function"><span class="type">const</span> std::vector&lt;StageRecord&gt;&amp; <span class="title">stage_records</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> stage_records_; &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ---- Streaming control ----</span></span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">stream_output_enabled</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> stream_output_; &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">set_stream_output</span><span class="params">(<span class="type">bool</span> v)</span> </span>&#123; stream_output_ = v; &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">layer_profile_enabled</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> layer_profile_enabled_; &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">set_layer_profile_enabled</span><span class="params">(<span class="type">bool</span> v)</span> </span>&#123; layer_profile_enabled_ = v; &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line"> <span class="comment">//三次 cudaMemGetInfo 的快照</span></span><br><span class="line">  <span class="type">size_t</span> mem_before_mb_ = <span class="number">0</span>;</span><br><span class="line">  <span class="type">size_t</span> mem_after_mb_ = <span class="number">0</span>;</span><br><span class="line">  <span class="type">size_t</span> mem_peak_mb_ = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//chrono 起点，给 tokenizer 等纯 CPU 工作计时</span></span><br><span class="line">  std::chrono::steady_clock::time_point cpu_start_;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//map&lt;string, CudaTimer&gt;，按名字缓存 GPU 计时器</span></span><br><span class="line">  std::map&lt;std::string, std::unique_ptr&lt;CudaTimer&gt;&gt; timers_;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//每次单次推理的原始数据：</span></span><br><span class="line">  <span class="comment">//一次完整 prompt → 最后一个 token 输出，包含 prefill + 全部 decode step</span></span><br><span class="line">  std::vector&lt;RunRecord&gt; runs_;</span><br><span class="line">  std::vector&lt;LayerModuleRecord&gt; layer_records_;<span class="comment">//每层耗时汇总</span></span><br><span class="line">  std::vector&lt;StageRecord&gt; stage_records_;<span class="comment">//每个阶段性耗时汇总</span></span><br><span class="line"></span><br><span class="line">  <span class="type">bool</span> stream_output_ = <span class="literal">true</span>;<span class="comment">//是否是流式输出</span></span><br><span class="line">  <span class="type">bool</span> layer_profile_enabled_ = <span class="literal">false</span>;<span class="comment">//是否开启每层同步计时</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// Helper for percentiles</span></span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="type">float</span> <span class="title">percentile</span><span class="params">(<span class="type">const</span> std::vector&lt;<span class="type">float</span>&gt;&amp; sorted, <span class="type">float</span> p)</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="5-模型集成"><a href="#5-模型集成" class="headerlink" title="5. 模型集成"></a>5. 模型集成</h4><p>在class model类里面新增了一个成员属性：  <code>profile::Profiler* profiler_ = nullptr</code>;</p><p>在调用cuda算子层之前，前后加入两个event<br><img src="/images/Pasted%20image%2020260723112330.png" alt="424"></p><h4 id="6-开始测量"><a href="#6-开始测量" class="headerlink" title="6. 开始测量"></a>6. 开始测量</h4><p><img src="/images/Pasted%20image%2020260723112543.png"></p><h4 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h4><ul><li><p><strong>benchmark</strong></p><ul><li><img src="/images/Pasted%20image%2020260723113015.png" alt="624"></li><li>可得：<ul><li><mark style="background:#fff88f">TTFT， prefill throughput</mark></li><li><mark style="background:#fff88f">itl(每token耗时), TPOT, p50， p95</mark></li><li><mark style="background:#fff88f">e2e 端到端延迟</mark></li></ul></li><li>这一步仅在prefill, decode之后，加入sync, 数量不多，因此对性能影响不大</li></ul></li><li><p><strong>layer profiler</strong></p><ul><li><img src="/images/Pasted%20image%2020260723113052.png" alt="600"></li><li>这一步，需要你自己针对每个layer，让cpu来sync等待gpu的算子结束运算，来粗略的统计每个layer的运行时间。<mark style="background:#ff4d4f">但是注意，由于sync次数很多，所以对整体性能影响大，只能用来统计每个layer的占比</mark></li></ul></li><li><p><strong>Nsight system</strong></p><ul><li>这一步就是查看时间具体花在哪一个kernel上。</li><li><img src="/images/Pasted%20image%2020260723164525.png" alt="544"></li></ul></li><li><p><strong>Nsight compute</strong></p><ul><li>具体分析kernel的设计</li></ul></li></ul><p>所以整个<strong>profile的性能分析流程</strong>应该是</p><p><img src="/images/Pasted%20image%2020260723164540.png"></p><p><img src="/images/Pasted%20image%2020260723164557.png"></p><h5 id="整个demo流程分析"><a href="#整个demo流程分析" class="headerlink" title="整个demo流程分析"></a>整个demo流程分析</h5><p>我们目前在demo的单prompt推理中，规定：</p><ul><li>prompt: 6个token<ul><li>prefill输入是5个（无需采样）</li><li>第六个token开始属于decode阶段</li></ul></li><li>最大产生长度16个token<br><img src="/images/img_v3_0213s_df0decb7-11df-4b33-9b0c-38e0820c073g.jpg" alt="621"></li></ul><p><strong>完成一次推理的过程如下</strong>：</p><p><img src="/images/Pasted%20image%2020260723192555.png" alt="444"><img src="/images/img_v3_0213s_e80e559c-15bf-44d0-a14f-1680d6d928ag.jpg" alt="243"></p><h5 id="profile流程分析"><a href="#profile流程分析" class="headerlink" title="profile流程分析"></a>profile流程分析</h5><p><img src="/images/Pasted%20image%2020260723203357.png"></p><p><img src="/images/Pasted%20image%2020260723203344.png"></p><p><strong>下面是性能分析报告</strong></p><p>分析结论：</p><ul><li><strong>decode阶段</strong><br><img src="/images/Pasted%20image%2020260723205252.png"><br>可以看到，输出单个token耗时4.46ms，对比<strong>benchmark</strong><br><img src="/images/Pasted%20image%2020260723205624.png" alt="405"></li></ul><p><img src="/images/Pasted%20image%2020260723205732.png"></p><p>实际我用gputimer测量下来发现，TPOT是4.6ms, cls_logits下的matmul也就500us，占比不大。不算特别瓶颈。</p><h5 id="128长度profile分析"><a href="#128长度profile分析" class="headerlink" title="128长度profile分析"></a>128长度profile分析</h5><p><strong>baseline 的朴素实现的benchmark</strong><br><img src="/images/Pasted%20image%2020260723222634.png"></p><p><strong>nsys分析</strong></p><p><strong><img src="/images/Pasted%20image%2020260723222722.png"></strong><br>可以看到，两个阶段</p><ul><li>prefill</li><li>decode</li></ul><h6 id="优化matmul算子，mha算子"><a href="#优化matmul算子，mha算子" class="headerlink" title="优化matmul算子，mha算子"></a>优化matmul算子，mha算子</h6><p><img src="/images/Pasted%20image%2020260723223938.png"><br>可以看到matmul算子耗时，以及mha算子的耗时最多，先优化一版本两个算子</p><p>目前先把老师的两个算子移植过来。</p><p>可以看到<strong>性能暴涨</strong><br><img src="/images/Pasted%20image%2020260724133332.png"></p><p>benchmark对比：<br>（前）<br><img src="/images/Pasted%20image%2020260724133447.png" alt="386"></p><p>（后）<br><img src="/images/Pasted%20image%2020260724133757.png" alt="350"></p><p>（前，可以看到cls_logits长时间，是由于matmul引起的，导致cudaFree一直在同步等待）<br><img src="/images/Pasted%20image%2020260724133843.png"></p><p>（后，可以看到，decoder阶段和cls，post阶段基本差不多）<br><img src="/images/Pasted%20image%2020260724133930.png"></p><h2 id="简历描述"><a href="#简历描述" class="headerlink" title="简历描述"></a>简历描述</h2><p><img src="/images/Pasted%20image%2020260725144720.png" alt="697"></p><h1 id="项目汇总"><a href="#项目汇总" class="headerlink" title="项目汇总"></a>项目汇总</h1><h2 id="项目一：Mini-CUDA-LLaMA-Inference-Engine"><a href="#项目一：Mini-CUDA-LLaMA-Inference-Engine" class="headerlink" title="项目一：Mini CUDA LLaMA Inference Engine"></a>项目一：Mini CUDA LLaMA Inference Engine</h2><p>目标：</p><figure class="highlight mipsasm"><table><tr><td class="code"><pre><span class="line">从零实现一个支持 <span class="keyword">LLaMA/Qwen </span>小模型单机推理的 CUDA 推理框架</span><br></pre></td></tr></table></figure><p>模块：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="bullet">1.</span> 工程基础</span><br><span class="line"><span class="bullet">   -</span> CMake</span><br><span class="line"><span class="bullet">   -</span> gdb / cuda-gdb</span><br><span class="line"><span class="bullet">   -</span> CUDA error check</span><br><span class="line"><span class="bullet">   -</span> Nsight Compute profiling</span><br><span class="line"></span><br><span class="line"><span class="bullet">2.</span> 基础框架</span><br><span class="line"><span class="bullet">   -</span> Tensor</span><br><span class="line"><span class="bullet">   -</span> DeviceBuffer</span><br><span class="line"><span class="bullet">   -</span> MemoryManager</span><br><span class="line"><span class="bullet">   -</span> Operator</span><br><span class="line"><span class="bullet">   -</span> Runtime</span><br><span class="line"><span class="bullet">   -</span> ModelConfig</span><br><span class="line"></span><br><span class="line"><span class="bullet">3.</span> 权重加载</span><br><span class="line"><span class="bullet">   -</span> mmap</span><br><span class="line"><span class="bullet">   -</span> 权重 shape 管理</span><br><span class="line"><span class="bullet">   -</span> CPU/GPU 权重加载</span><br><span class="line"><span class="bullet">   -</span> 算子与权重绑定</span><br><span class="line"></span><br><span class="line"><span class="bullet">4.</span> CUDA 算子</span><br><span class="line"><span class="bullet">   -</span> RMSNorm</span><br><span class="line"><span class="bullet">   -</span> RoPE</span><br><span class="line"><span class="bullet">   -</span> MatMul</span><br><span class="line"><span class="bullet">   -</span> Softmax</span><br><span class="line"><span class="bullet">   -</span> Attention</span><br><span class="line"><span class="bullet">   -</span> MLP</span><br><span class="line"><span class="bullet">   -</span> Sampling</span><br><span class="line"></span><br><span class="line"><span class="bullet">5.</span> LLaMA 推理</span><br><span class="line"><span class="bullet">   -</span> Tokenizer</span><br><span class="line"><span class="bullet">   -</span> Prefill</span><br><span class="line"><span class="bullet">   -</span> Decode</span><br><span class="line"><span class="bullet">   -</span> KV Cache</span><br><span class="line"><span class="bullet">   -</span> Generate Loop</span><br><span class="line"></span><br><span class="line"><span class="bullet">6.</span> 性能优化</span><br><span class="line"><span class="bullet">   -</span> 向量化访存</span><br><span class="line"><span class="bullet">   -</span> shared memory 优化</span><br><span class="line"><span class="bullet">   -</span> warp reduce</span><br><span class="line"><span class="bullet">   -</span> kernel fusion</span><br><span class="line"><span class="bullet">   -</span> 显存复用</span><br><span class="line"><span class="bullet">   -</span> Nsight 分析报告</span><br></pre></td></tr></table></figure><hr><h2 id="项目二：Mini-vLLM-nano-vLLM-Optimization"><a href="#项目二：Mini-vLLM-nano-vLLM-Optimization" class="headerlink" title="项目二：Mini-vLLM &#x2F; nano-vLLM Optimization"></a>项目二：Mini-vLLM &#x2F; nano-vLLM Optimization</h2><p>目标：</p><figure class="highlight lasso"><table><tr><td class="code"><pre><span class="line">基于 nano<span class="params">-vLLM</span> 实现高吞吐推理调度和 KV <span class="keyword">Cache</span> 管理优化</span><br></pre></td></tr></table></figure><p>模块：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="bullet">1.</span> vLLM 基础机制</span><br><span class="line"><span class="bullet">   -</span> PagedAttention</span><br><span class="line"><span class="bullet">   -</span> Block Manager</span><br><span class="line"><span class="bullet">   -</span> KV Cache Block</span><br><span class="line"><span class="bullet">   -</span> Sequence / Request / Batch</span><br><span class="line"></span><br><span class="line"><span class="bullet">2.</span> 调度系统</span><br><span class="line"><span class="bullet">   -</span> waiting queue</span><br><span class="line"><span class="bullet">   -</span> running queue</span><br><span class="line"><span class="bullet">   -</span> prefill / decode 分离</span><br><span class="line"><span class="bullet">   -</span> continuous batching</span><br><span class="line"><span class="bullet">   -</span> chunked prefill</span><br><span class="line"></span><br><span class="line"><span class="bullet">3.</span> Cache 优化</span><br><span class="line"><span class="bullet">   -</span> prefix cache</span><br><span class="line"><span class="bullet">   -</span> block reuse</span><br><span class="line"><span class="bullet">   -</span> cache eviction</span><br><span class="line"><span class="bullet">   -</span> cache hit rate 统计</span><br><span class="line"></span><br><span class="line"><span class="bullet">4.</span> 性能评测</span><br><span class="line"><span class="bullet">   -</span> TTFT</span><br><span class="line"><span class="bullet">   -</span> TPOT</span><br><span class="line"><span class="bullet">   -</span> Throughput</span><br><span class="line"><span class="bullet">   -</span> KV Cache 使用率</span><br><span class="line"><span class="bullet">   -</span> 显存占用</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">从 CMake、C++ 与 CUDA 基础组件入手，记录手写 LLaMA 推理框架的 tensor、算子和 kernel 设计。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="推理系统" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E6%8E%A8%E7%90%86%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="GTest" scheme="https://liangji-seu.github.io/tags/GTest/"/>
    
    <category term="C++" scheme="https://liangji-seu.github.io/tags/C/"/>
    
    <category term="LLaMA" scheme="https://liangji-seu.github.io/tags/LLaMA/"/>
    
    <category term="CMake" scheme="https://liangji-seu.github.io/tags/CMake/"/>
    
  </entry>
  
  <entry>
    <title>infra 面经</title>
    <link href="https://liangji-seu.github.io/2026/05/12/infra-%E9%9D%A2%E7%BB%8F%E6%95%B4%E7%90%86/"/>
    <id>https://liangji-seu.github.io/2026/05/12/infra-%E9%9D%A2%E7%BB%8F%E6%95%B4%E7%90%86/</id>
    <published>2026-05-12T14:49:55.000Z</published>
    <updated>2026-10-03T10:34:31.629Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/Snipaste_2026-05-12_22-28-58.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-03.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-14.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-24.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-30.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-35.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-41.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-46.png"></p><p><img src="/images/Snipaste_2026-05-12_22-29-51.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-07.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-12.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-17.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-21.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-26.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-30.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-35.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-47.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-52.png"></p><p><img src="/images/Snipaste_2026-05-12_22-30-57.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-04.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-10.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-21.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-31.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-39.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-46.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-52.png"></p><p><img src="/images/Snipaste_2026-05-12_22-31-57.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-03.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-14.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-19.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-25.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-33.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-39.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-44.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-50.png"></p><p><img src="/images/Snipaste_2026-05-12_22-32-56.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-07.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-13.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-20.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-26.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-32.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-33-51.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-04.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-10.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-20.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-26.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-32.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-34-48.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-05.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-11.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-17.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-22.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-29.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-34.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-41.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-47.png"></p><p><img src="/images/Snipaste_2026-05-12_22-35-53.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-00.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-14.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-21.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-27.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-35.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-43.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-49.png"></p><p><img src="/images/Snipaste_2026-05-12_22-36-55.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-13.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-19.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-25.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-33.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-46.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-51.png"></p><p><img src="/images/Snipaste_2026-05-12_22-37-59.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-05.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-13.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-19.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-26.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-32.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-41.png"></p><p><img src="/images/Snipaste_2026-05-12_22-38-53.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-02.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-09.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-16.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-22.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-28.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-35.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-41.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-49.png"></p><p><img src="/images/Snipaste_2026-05-12_22-39-55.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-03.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-09.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-15.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-25.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-31.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-38.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-46.png"></p><p><img src="/images/Snipaste_2026-05-12_22-40-52.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-00.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-05.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-12.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-18.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-24.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-30.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-36.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-49.png"></p><p><img src="/images/Snipaste_2026-05-12_22-41-56.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-05.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-11.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-19.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-25.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-32.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-38.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-45.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-51.png"></p><p><img src="/images/Snipaste_2026-05-12_22-42-56.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-13.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-20.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-28.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-34.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-47.png"></p><p><img src="/images/Snipaste_2026-05-12_22-43-54.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-08.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-15.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-29.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-36.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-42.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-48.png"></p><p><img src="/images/Snipaste_2026-05-12_22-44-57.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-03.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-11.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-18.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-23.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-28.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-33.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-45.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-50.png"></p><p><img src="/images/Snipaste_2026-05-12_22-45-55.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-01.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-07.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-12.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-19.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-27.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-33.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-39.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-46.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-53.png"></p><p><img src="/images/Snipaste_2026-05-12_22-46-59.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-04.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-10.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-16.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-23.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-30.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-38.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-44.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-50.png"></p><p><img src="/images/Snipaste_2026-05-12_22-47-58.png"></p><p><img src="/images/Snipaste_2026-05-12_22-48-04.png"></p><p><img src="/images/Snipaste_2026-05-12_22-48-12.png"></p><p><img src="/images/Snipaste_2026-05-12_22-48-17.png"></p><p><img src="/images/Snipaste_2026-05-12_22-48-23.png"></p><p><img src="/images/Snipaste_2026-05-12_22-48-29.png"></p>]]></content>
    
    
    <summary type="html">以图片整理 AI Infra 与大模型方向面试题，作为后续复盘与查漏补缺的入口。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="面试复盘" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E9%9D%A2%E8%AF%95%E5%A4%8D%E7%9B%98/"/>
    
    
    <category term="AI Infra" scheme="https://liangji-seu.github.io/tags/AI-Infra/"/>
    
    <category term="CUDA" scheme="https://liangji-seu.github.io/tags/CUDA/"/>
    
    <category term="C++" scheme="https://liangji-seu.github.io/tags/C/"/>
    
    <category term="LLM" scheme="https://liangji-seu.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>深度学习基础-pytorch+python</title>
    <link href="https://liangji-seu.github.io/2026/04/28/infra-%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%9F%BA%E7%A1%80-pytorch+python/"/>
    <id>https://liangji-seu.github.io/2026/04/28/infra-%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%9F%BA%E7%A1%80-pytorch+python/</id>
    <published>2026-04-28T06:21:00.000Z</published>
    <updated>2026-10-03T10:34:31.628Z</updated>
    
    <content type="html"><![CDATA[<h1 id="python概念扫盲"><a href="#python概念扫盲" class="headerlink" title="python概念扫盲"></a>python概念扫盲</h1><p>基础的python语法，这里就不讲了，python本身底层是用c++来实现的，所以全部都是面向对象变成，里面所有的库，都是实现的类。</p><p>python的所有变量 &#x3D; auto指针<br>指针创建在栈区，指向堆区的内存。实际的对象内存，在堆区里面。</p><h1 id="pytorch"><a href="#pytorch" class="headerlink" title="pytorch"></a>pytorch</h1><h2 id="pytorch特性-："><a href="#pytorch特性-：" class="headerlink" title="&#x3D;&#x3D;pytorch特性&#x3D;&#x3D;："></a><strong>&#x3D;&#x3D;pytorch特性&#x3D;&#x3D;</strong>：</h2><p><img src="/images/Pasted%20image%2020260428211057.png"></p><p>比较重要的就是：</p><ol><li>动态计算图的理解-&gt; 自动微分的实现</li><li>torch.tensor(), 张量，和numpy库里面的数组定义类似。</li></ol><h2 id="自动求导机制"><a href="#自动求导机制" class="headerlink" title="&#x3D;&#x3D;自动求导机制&#x3D;&#x3D;"></a>&#x3D;&#x3D;<strong>自动求导机制</strong>&#x3D;&#x3D;</h2><p>我们模型的Loss函数，需要对w, b求导，其自动求导是依赖计算图的。 当我们建立好了计算图</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">w → * → y_pred → - → (差) → **<span class="number">2</span> → loss</span><br><span class="line">    ↑                 ↑</span><br><span class="line">    x               y_true</span><br><span class="line">    </span><br><span class="line">    w = torch.tensor(<span class="number">2.0</span>, requires_grad=True)</span><br><span class="line">    loss.backward()</span><br></pre></td></tr></table></figure><p>我们通过标记变量为需要求导，相当于把这个对象的标志位，置1，这样当你调用loss.backward()的时候，他就可以根据这个计算链表图，找到对应的变量，求Loss()关于这个变量的偏微分，也就是偏导数。所有的偏导数的动态数组，就是梯度</p><h2 id="pytorch的作用"><a href="#pytorch的作用" class="headerlink" title="&#x3D;&#x3D;pytorch的作用&#x3D;&#x3D;"></a><strong>&#x3D;&#x3D;pytorch的作用&#x3D;&#x3D;</strong></h2><p>我们使用pytorch，本质上，是为了快速的</p><ul><li>搭建网络模型</li><li>处理数据</li><li>训练&#x2F;评估</li><li>推理<br>本质上都是在干这4件事情。所以需要学会用哪些库来实现我们想要的功能。</li></ul><h2 id="pytorch库架构图"><a href="#pytorch库架构图" class="headerlink" title="&#x3D;&#x3D;pytorch库架构图&#x3D;&#x3D;"></a><strong>&#x3D;&#x3D;pytorch库架构图&#x3D;&#x3D;</strong></h2><p>pytorch是一个大的库，里面有不同功能模块子库<br><img src="/images/Pasted%20image%2020260428211528.png"></p><h2 id="自动微分"><a href="#自动微分" class="headerlink" title="自动微分"></a>自动微分</h2><p>pytorch里最重要的就是依靠动态计算图，来进行自动微分的计算了。</p><h3 id="梯度"><a href="#梯度" class="headerlink" title="梯度"></a>梯度</h3><p>一元函数，因变量关于自变量求导，叫做导数。<br>y &#x3D; f(x)  &#x3D;&gt;  ∂f&#x2F;∂x（导数）<br>多元函数，因变量关于自变量们求导，叫做梯度。<br>y &#x3D; f(x1, x2, x3,….., xn) &#x3D;&gt;<br>{∂f&#x2F;∂x1, ∂f&#x2F;∂x2, ∂f&#x2F;∂x3, …… , ∂f&#x2F;∂xn} （叫做y的梯度）<br>所以<mark style="background:rgba(163, 67, 31, 0.2)">梯度本身，就是这个多元函数（标量），关于各个自变量的导数</mark>。</p><p><img src="/images/Pasted%20image%2020260428212737.png"></p><h3 id="torch库自动计算梯度"><a href="#torch库自动计算梯度" class="headerlink" title="torch库自动计算梯度"></a>torch库自动计算梯度</h3><p>上面我们定义好了梯度的计算公式，下面就是torch库，来帮我们计算梯度</p><p>我们不需要自己定义求梯度的公式。在构建前向传播的损失函数的时候。torch利用动态计算图，来表达出计算的公式。然后利用<strong>链式求导</strong>法则，来进行求导。<br><img src="/images/4d9fa9e1a77e75d9eeee8d3ff9e162ad.jpg"></p><p>可以看到，通过链式法则，就可以把求梯度的过程，和计算图的每个环节联系上。</p><p>通过一次<mark style="background:rgba(163, 67, 31, 0.2)">loss.backward()</mark>, 就可以计算一次loss(x1, x2, x3, …, xn)的梯度了（同时路上所有中间变量的梯度也都求出来了。）</p><p><img src="/images/ce9e727371321e54859b9ff0f2112171.jpg"></p><blockquote><p>注意：<strong>n元函数</strong>loss，输入是n元向量，<strong>loss的梯度</strong>，也是一个<strong>n元向量</strong>，是关于所有自变量的向量<br>因此，梯度向量的每个元素，都应该存放在n元输入向量的每个元素对象的<strong>内存</strong>里面。<br>所以输入元素<mark style="background:rgba(240, 200, 0, 0.2)">x1.grad</mark>， <mark style="background:rgba(240, 200, 0, 0.2)">x2.grad,</mark> ….,<mark style="background:rgba(240, 200, 0, 0.2)"> xn.grad</mark>. 这个成员变量，专门用来存放梯度元素分量</p></blockquote><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">x = torch.randn(<span class="number">2</span>,<span class="number">2</span>, requires_grad=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(x)</span><br><span class="line"></span><br><span class="line">y = x + <span class="number">2</span></span><br><span class="line">z = y * y * <span class="number">3</span></span><br><span class="line"><span class="built_in">print</span>(z)</span><br><span class="line">out = z.mean()</span><br><span class="line"><span class="built_in">print</span>(out)</span><br><span class="line"></span><br><span class="line">out.backward()</span><br><span class="line"><span class="built_in">print</span>(x.grad)</span><br></pre></td></tr></table></figure><p>通过标记x这个变量的内部标志位 <mark style="background:#affad1">requires_grad</mark>&#x3D;True, 之后，整个计算图只要包含x这个变量，全部会被标记这个标志位。</p><p><img src="/images/5c6b7683240dd461a33b50d78be89db2.jpg"></p><p>之后out.backward(), 触发torch的求导过程，并把out(x), 关于各个自变量，中间环节的导数（梯度分类），全部存放在对于的对象内存里面。</p><p><strong><mark style="background:#ff4d4f">梯度本身也是一个关于(x1, x2,x3, ….., xn)的n元函数</mark></strong>。</p><p>每次计算的梯度都不一样，当然不一样了，<strong>梯度也是一个函数</strong>（关于x的函数）<br><img src="/images/Pasted%20image%2020260428215003.png"></p><h3 id="python的with-语句"><a href="#python的with-语句" class="headerlink" title="python的with 语句"></a>python的with 语句</h3><p>python里面，with语句，通常用来申请某类资源，然后里面实现在占用这个资源时要做的事情，跳出这个过程之后，会<strong>自动释放这个资源</strong></p><h3 id="torch-no-grad"><a href="#torch-no-grad" class="headerlink" title="torch.no_grad()"></a>torch.no_grad()</h3><p>用torch.no_grad(), 可以暂时关闭自动求导的标志位传到, 相当于关中断一样。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义一个需要梯度的张量</span></span><br><span class="line">x = torch.tensor([<span class="number">1.0</span>, <span class="number">2.0</span>], requires_grad=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 正常操作：会追踪梯度</span></span><br><span class="line">y1 = x * <span class="number">2</span></span><br><span class="line"><span class="built_in">print</span>(y1.requires_grad)  <span class="comment"># 输出 True，会被加入计算图</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 用 torch.no_grad()：块内操作不追踪梯度</span></span><br><span class="line"><span class="keyword">with</span> torch.no_grad():</span><br><span class="line">    y2 = x * <span class="number">2</span></span><br><span class="line">    <span class="built_in">print</span>(y2.requires_grad)  <span class="comment"># 输出 False，不加入计算图</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 块外恢复正常</span></span><br><span class="line">y3 = x * <span class="number">2</span></span><br><span class="line"><span class="built_in">print</span>(y3.requires_grad)  <span class="comment"># 输出 True，恢复追踪</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260428215400.png"></p><hr><h1 id="深度学习的组成"><a href="#深度学习的组成" class="headerlink" title="深度学习的组成"></a>深度学习的组成</h1><h2 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h2><p>我们的torch深度学习，主要做一下几件事：</p><ol><li><strong>数据</strong> [[#torch 数据]]</li><li><strong>模型</strong> [[#torch 模型]]</li><li>衡量模型质量（定义<strong>损失函数</strong>）</li><li>训练<ol><li>前向传播</li><li>计算损失（得到这次的模型性能）</li><li>反向传播（计算参数更新值）</li><li>参数更新（优化器来具体更新参数）</li></ol></li><li>评估&#x2F;推理</li></ol><h2 id="网络层-架构分类"><a href="#网络层-架构分类" class="headerlink" title="网络层&#x2F;架构分类"></a>网络层&#x2F;架构分类</h2><ul><li>FNN（前馈神经网络）[[#FNN 前馈]]</li><li>CNN（卷积神经网络）[[#CNN 卷积]]</li><li>RNN（循环神经网络）</li><li>Transformer （注意力机制）</li></ul><h2 id="一些小零件"><a href="#一些小零件" class="headerlink" title="一些小零件"></a>一些小零件</h2><ul><li>各层之间的激活函数[[#激活函数]]</li><li>各种损失函数</li><li>各种优化器</li><li>softmax层 [[#softmax运算]]</li></ul><h2 id="一些技巧"><a href="#一些技巧" class="headerlink" title="一些技巧"></a>一些技巧</h2><ul><li>[[#dropout 暂退法]]</li><li>[[#权重衰退（正则化）]]</li><li>[[#批量规范化]]</li></ul><h1 id="理论角度理解深度学习"><a href="#理论角度理解深度学习" class="headerlink" title="理论角度理解深度学习"></a>理论角度理解深度学习</h1><h2 id="三大件"><a href="#三大件" class="headerlink" title="三大件"></a>三大件</h2><p>首先这一节，我从数学理论的角度来理解，深度学习的过程。简单来说，深度学习主要的三大件：</p><ul><li><strong>数据(x,y’)</strong></li><li><strong>模型（参数（a,b））</strong></li><li><strong>损失函数（平均损失函数Loss）</strong></li></ul><p>本质上，我们定义&#x2F;训练模型，依赖的是这样的架构：<br><img src="/images/b601338da71337a1647f935b69f273b0.jpg"></p><p>我们现在假设一个最简单的函数模型（即线性回归模型）：<br><img src="/images/f6e2ee0930f6de81ec519a777d857295.jpg" alt="286"></p><p><img src="/images/8b52c7545a89cd7649c21260576c1885.jpg" alt="566"></p><p><strong><mark style="background:#ff4d4f">模型</mark></strong>：<br>y &#x3D; ax + b<br>所以**<mark style="background:#fff88f">模型参数为（a, b）</mark>**</p><p><strong><mark style="background:#ff4d4f">数据</mark></strong>：<br>我这里一共准备了8个样本：<mark style="background:#fff88f"> 一个样本 &#x3D; 一个输入+一个标签</mark><br>所以dataset就是这8个样本组成的张量<br>dataloader是针对这个dataset的迭代器，设定一共分成batch_size批：</p><ul><li>full_batch：batch_size &#x3D; 1<ul><li>训练稳定，但是内存占用大</li></ul></li><li>mini_batch: batch_size &#x3D; 4<ul><li>训练速度快，内存占用小（后面分析）</li></ul></li></ul><p>**<mark style="background:#ff4d4f">损失函数：</mark>loss</p><p>这里就先使用最简单的<strong>平方误差函数</strong></p><ul><li><strong>单个样本的损失loss</strong> </li><li><img src="/images/Pasted%20image%2020260429133123.png"><br><img src="/images/9343d0b924a576a527488d32dd699dc1.jpg" alt="364"></li></ul><p>但是我们实际训练的时候，是不可能用单个样本的loss损失来衡量模型当前的质量的（万一这个样本本身不太好，对吧）</p><p>**<mark style="background:#ff4d4f">平均损失函数</mark>Loss<br>所以我们选择一个batch，一个批的样本，来衡量模型的质量。 <strong>为了度量模型在整个数据集上的质量，我们需计算在训练集个样本上的损失均值（也等价于求和）</strong><br><img src="/images/Pasted%20image%2020260429133508.png" alt="587"><br><img src="/images/c7657f258b925ab32163912d16eec2f2.jpg"></p><p><strong>所以我们最终会使用这个平均损失函数Loss，来作为衡量模型质量的标准。</strong></p><h2 id="优化模型"><a href="#优化模型" class="headerlink" title="优化模型"></a>优化模型</h2><p>我们获得了数据集，定义了模型，然后设置了Loss损失函数，来衡量模型的好坏。</p><p>最初，输入输入到模型（经过模型参数运算），得到输出，但是这个和真值标签是有差距的。</p><p>所以我们<strong>训练模型的目的</strong>，就是为了，<strong>获得一个最佳的模型参数，让这个模型的损失函数最小</strong>。</p><p>所以，<strong>训练</strong>本质上是围绕着<strong>损失函数</strong>来进行的。</p><p>为什么损失函数能作为训练的依赖呢，因为损失函数，主要由两块组成：</p><ul><li>模型参数</li><li>数据（输入+标签）</li></ul><p><img src="/images/b601338da71337a1647f935b69f273b0.jpg"></p><p>可以看到，Loss的组成里面，<strong>损失函数本身只是一个组合模型参数和数据的框架</strong>。绿色里面是我们的模型参数+数据集。</p><p>其中红色是模型参数，蓝色是我们的数据集。</p><p>当给定<strong>数据集</strong>后（即给定<mark style="background:#fff88f">输入x1, x2, ….., xn, 标签y1’, y2’, …., yn’</mark>）数据集就作为Loss(a,b)这个函数的系数，然后我们当前模型的参数(a1,b1)，只是这个Loss(a,b)曲线上的一个点。</p><p>我们的<strong>目的，是为了找一个(a2,b2), 当下一次再次输入这个数据集的时候</strong>（所以Loss(a,b)的系数没有变），<strong>Loss的值能够变小</strong>。</p><p>这就用到了<mark style="background:#affad1">梯度下降</mark></p><p><img src="/images/84cecf9c1676010fcaa5833f16feef31.jpg"></p><p>上面举了一个full-batch的例子，当只有一个batch的时候，Loss的图像本身是不会变的。因为系数不变。如果mini-batch，有多个batch，会变化，但是训练的更快了。</p><p><strong>为什么我们使用mini-batch？</strong><br>因为full-batch, 相当于一个batch里面包括了所有的样本，全部输入完了才更新模型参数。<br>这会导致Loss里面的n项（n个样本）太大，内存不停的占用。导致爆内存。</p><p><img src="/images/f5859d7b24a45d37039d78a200783b81.jpg" alt="322"></p><p>所以，这就解释了，Loss损失函数，模型参数model，数据集dataset, 这三者是一个整体，要一起讨论的。</p><h3 id="举例证明"><a href="#举例证明" class="headerlink" title="举例证明"></a>举例证明</h3><p>下面来看一下我们训练的过程<br><img src="/images/8486afa0181091e1ab61a55c248c0792.jpg"></p><h3 id="Loss函数求w-b的梯度"><a href="#Loss函数求w-b的梯度" class="headerlink" title="Loss函数求w,b的梯度"></a>Loss函数求w,b的梯度</h3><p>batch_size</p><ul><li>&#x3D; 一个batch批，里面有多少个样本</li><li>&#x3D; Loss(w,b) &#x3D; 1&#x2F;2n * ∑((wx1+b-y1’)2+(wx2+b-y2’)2+…+(wxn+b-yn’)2) 里面有多少项</li></ul><p>所以当batch_size &gt; 1的时候。<br><strong>Loss(w,b)关于(w,b)求出来的梯度，本质上是平均梯度。已经除了batch_size了</strong>。<br><img src="/images/Pasted%20image%2020260429155539.png" alt="84"></p><p><img src="/images/6beffac0fe10deff660fef04ff09d8ef.jpg" alt="513"></p><h3 id="多层FNN时链式法则，反向传播更新梯度的过程"><a href="#多层FNN时链式法则，反向传播更新梯度的过程" class="headerlink" title="多层FNN时链式法则，反向传播更新梯度的过程"></a>多层FNN时链式法则，反向传播更新梯度的过程</h3><p>^e096f0</p><p>假设现在有一个双层的全连接网络，第一层一般用激活函数。下面给出证明过程</p><p>一般我们习惯:全连接层的：</p><ol><li>输入：x</li><li>权重矩阵W，偏置向量b</li><li>输出（非最后一层叫预激活）： z</li><li>激活函数sigma</li></ol><p><img src="/images/d312243badf6ba8ea3978a515f88c8fa.jpg" alt="593"><br><img src="/images/ddbec40e2d05273036326d6decafd12e.jpg" alt="584"></p><p>所以总结下来：<strong>每一层参数的梯度</strong>，仅和 </p><ol><li><strong>反向传过来的上游误差</strong>：就是<strong>后面所有层 + 损失，对当前层输入的导数</strong></li><li><strong>当前层激活函数的局部导数</strong> </li><li><strong>当前层线性部分（(z&#x3D;Wx+b)）对自身参数的局部导数</strong></li></ol><p>有关。<mark style="background:#fff88f">所以，每一层的参数的梯度，仅和自身层及后层有关</mark>。</p><h2 id="优化参数"><a href="#优化参数" class="headerlink" title="优化参数"></a>优化参数</h2><p>我们举得例子，就是最简单的y &#x3D;ax + b, 就是最简单的一维输入，一维输出的简单<strong>线性回归模型</strong></p><p>包括我们后面多维输入构成的FNN全连接层，都是这个训练逻辑，这样就很好理解了。</p><p>那么，扩展到一般的线性回归模型： y&#x3D;wx + b, 其中w，b均为张量。</p><p>我们<strong>模型的参数就是（w,b）</strong>, 我们训练的目的，就是<strong>更新这个（w,b）</strong>, 让<strong>Loss损失函数min</strong></p><p><mark style="background:#fff88f">那么具体更新参数呢？</mark><br>前面只讲了，Loss对w,b求微分，得到关于（w,b）的平均梯度。我<strong>只知道方向要往左&#x2F;右变化</strong>，<strong>那么变化多少呢？</strong></p><p>这个就是<mark style="background:#ff4d4f">优化器</mark>**要干的事情了</p><p>他决定了如何更新模型的参数，减去一半？还是减去多少？</p><p>下面展示一个最简单的优化器：<br><img src="/images/Pasted%20image%2020260429140002.png" alt="454"><br>具体可以写成<br><img src="/images/Pasted%20image%2020260429140405.png" alt="490"><br><img src="/images/Pasted%20image%2020260429140445.png" alt="186"><br><img src="/images/Pasted%20image%2020260429140457.png" alt="86"></p><p>注意，这里的平均梯度，就是我们自动求导计算出来的了。我们优化器仅有两个参数：</p><ul><li>模型参数（w,b）<ul><li>平均梯度已经存放在参数的对象里面了。</li></ul></li><li>学习率（lr）<br><img src="/images/Pasted%20image%2020260429155658.png" alt="240"><br>这里的批量大小，学习率，就是我们的超参数<blockquote><p><strong>batch_size</strong>, 就是表示的<strong>一个batch里面有多少个样本</strong>。</p></blockquote></li></ul><p><strong><mark style="background:#ff4d4f">梯度下降得到什么？</mark></strong><br>我们一个batch，能够得到batch_size个分类的Loss损失函数，然后Loss关于w,b求导。得到的是</p><p>我们不需要具体关心如何更新参数，直接选用<strong>torch库里面的优化器</strong>就行了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>至此我们从理论层面上讲解推导了</p><ul><li>数据</li><li>模型+参数</li><li>损失函数<ul><li>优化器<br>的作用。</li></ul></li></ul><hr><h1 id="torch-数据"><a href="#torch-数据" class="headerlink" title="torch 数据"></a>torch 数据</h1><p>这一节，来讲一下，torch库，是如何处理模型所需要的数据的</p><p>torch库里面专门用来处理数据的库是：</p><ul><li><code>torch.utils.data.Dataset</code> torch库的<strong>数据集类</strong></li><li><code>torch.utils.data.DataLoader</code> torch库的<strong>数据集迭代器类</strong>（帮助获取数据的类）<blockquote><p>既然都是类，那么要用，肯定要创建各自的对象，才能实际发挥作用。</p></blockquote></li></ul><h3 id="创建自定义数据集"><a href="#创建自定义数据集" class="headerlink" title="创建自定义数据集"></a>创建自定义数据集</h3><p>创建自定义的数据集，需要自己实现一个自定义数据集类（集成Dataset基类），然后为迭代器提供两个必要的方法。用来告诉迭代器如何访问你的自定义数据集。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch.utils.data <span class="keyword">import</span> Dataset, DataLoader</span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义 自定义数据集类，继承Dataset基类</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyDataset</span>(<span class="title class_ inherited__">Dataset</span>):</span><br><span class="line"><span class="comment"># 类的构造函数下半段， 获取我的数据集内容</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, X_data, Y_data</span>):</span><br><span class="line">        <span class="variable language_">self</span>.X_data = X_data</span><br><span class="line">        <span class="variable language_">self</span>.Y_data = Y_data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 实现内置__len__方法，用来返回数据集的数据长度</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__len__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.X_data)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 实现内置__getitem__方法，用来返回一条张量的数据样本</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getitem__</span>(<span class="params">self, idx</span>):</span><br><span class="line">        x = torch.tensor(<span class="variable language_">self</span>.X_data[idx], dtype = torch.float32)</span><br><span class="line">        y = torch.tensor(<span class="variable language_">self</span>.Y_data[idx], dtype = torch.float32)</span><br><span class="line">        <span class="keyword">return</span> x, y</span><br><span class="line"></span><br><span class="line"><span class="comment"># 这是我们自定义的数据X（输入），Y（标签）</span></span><br><span class="line">X_data = np.arange(<span class="number">8</span>).reshape(<span class="number">4</span>,<span class="number">2</span>) <span class="comment">#先用numpy创建一个4x2的 0-7的数组</span></span><br><span class="line">Y_data = [<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建出我们自己的数据集对象dataset</span></span><br><span class="line">dataset = MyDataset(X_data, Y_data)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建一个数据集迭代器，用来替我们自由的随机选择数据集。</span></span><br><span class="line"><span class="string">&quot;&quot;&quot; 迭代器设置：指定数据集对象， batch_size批数（分成batch_size批），shuffle表示随机选取&quot;&quot;&quot;</span></span><br><span class="line">dataloader = DataLoader(dataset, batch_size=<span class="number">2</span>, shuffle=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>):</span><br><span class="line">    <span class="keyword">for</span> batch_idx, (inputs, labels) <span class="keyword">in</span> <span class="built_in">enumerate</span>(dataloader):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;batch index = &quot;</span>, batch_idx)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;(inputs, labels) = &quot;</span>, (inputs, labels))</span><br></pre></td></tr></table></figure><p><img src="/images/aeb02221b8034f0046e6deb51189b886.jpg"></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">batch index =  0</span><br><span class="line">(inputs, labels) =  (tensor([[0., 1.],</span><br><span class="line">        [6., 7.]]), tensor([1., 0.]))</span><br><span class="line">batch index =  1</span><br><span class="line">(inputs, labels) =  (tensor([[2., 3.],</span><br><span class="line">        [4., 5.]]), tensor([0., 1.]))</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">batch index =  0</span><br><span class="line">(inputs, labels) =  (tensor([[2., 3.],</span><br><span class="line">        [6., 7.]]), tensor([0., 0.]))</span><br><span class="line">batch index =  1</span><br><span class="line">(inputs, labels) =  (tensor([[0., 1.],</span><br><span class="line">        [4., 5.]]), tensor([1., 1.]))</span><br></pre></td></tr></table></figure><p>可以看到，连续两次运行后，enumerate( dataloader), 枚举迭代器让它一次返回一个迭代单位</p><ul><li>第x&#x2F;2批数据样本</li><li>随机返回抽取</li></ul><p>所以可以看到，每次返回的是2x2的，也就是<strong>随机两个样本</strong>组成的<strong>张量</strong>，也就是<strong>一个batch</strong>，<strong>一个批次</strong></p><blockquote><p>从这里就可以看出，深度学习的训练，对数据的使用，是以批为单位的。一批才计算一次损失，更新一次模型参数。</p></blockquote><h3 id="预定义数据集-数据预处理"><a href="#预定义数据集-数据预处理" class="headerlink" title="预定义数据集 + 数据预处理"></a>预定义数据集 + 数据预处理</h3><p>torch里面还内置了一些比较知名的数据集。比如手写数字图片MNIST数据集。</p><p>同时，我们针对数据，肯定也希望有一些管线的预处理，来实现数据增强。比如图片的翻转，裁剪，数据范围的归一化等等。</p><p>我们这里使用的MNIST，手写图片，是属于CV领域的，所以是定义在torchvision模块库里面的。</p><ul><li><code>torchvision.transforms</code> (数据处理库)<ul><li><code>transforms.ToTensor()</code> <ul><li>openCV的图片格式是(<strong>高度</strong>x<strong>宽度</strong>x<strong>通道</strong>)，里面<strong>每个元素</strong>的范围是<strong>0-255</strong></li><li>.ToTensor()转换维度格式化（<strong>通道</strong>x<strong>高度</strong>x<strong>宽度</strong>），元素取值范围&#x2F;255（也就是0-1）<img src="/images/Pasted%20image%2020260428224503.png"></li><li>这样将opencv的图片格式转成 卷积能用的张量。</li></ul></li><li><code>transforms.Normalize(mean, std)</code> 调整每个像素的值的范围从（0-1）到（-1-1），这样方便收敛，因为以0为中心<ul><li><img src="/images/Pasted%20image%2020260428224650.png"></li><li><img src="/images/Pasted%20image%2020260428224711.png"></li><li><img src="/images/Pasted%20image%2020260428224727.png"></li></ul></li></ul></li><li><code>torchvision.datasets</code> (<strong>官方提供</strong>的“素材库”和“工具箱”)<ul><li>是一个内置数据集的库，内置了CV领域的经典公开数据集。</li><li><img src="/images/Pasted%20image%2020260428224913.png"></li></ul></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span>  torch</span><br><span class="line"><span class="keyword">import</span> torchvision.transforms <span class="keyword">as</span> transforms</span><br><span class="line"><span class="keyword">import</span> torchvision.datasets <span class="keyword">as</span> datasets</span><br><span class="line"></span><br><span class="line"><span class="comment"># 这里我们预定义一个数据处理流（一组数据处理函数，用来实现一系列的数据处理）</span></span><br><span class="line">transform = transforms.Compose([</span><br><span class="line">    transforms.ToTensor(), <span class="comment">#转成tensor张量格式</span></span><br><span class="line">    transforms.Normalize(mean=[<span class="number">0.5</span>], std=[<span class="number">0.5</span>]) <span class="comment">#归一化，核心作用是将图像的像素值分布调整到一个特定的范围（通常是 0 均值附近），从而加速模型的收敛</span></span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;开始加载经典MNIST手写数据集</span></span><br><span class="line"><span class="string">dataset.MNIST()库函数提供下载，读取方式</span></span><br><span class="line"><span class="string">- root=&quot;./data&quot;  指定数据集的存储目录，下载后其他项目也能指定使用</span></span><br><span class="line"><span class="string">- train=True     区分训练集60000个样本/测试集10000个样本。</span></span><br><span class="line"><span class="string">- transform=transform    指定每个数据样本（一张图片）的加工流水线，处理成能输入CNN的张量tensor</span></span><br><span class="line"><span class="string">- download=True   自动下载开关（打开后，会自动检查root下是否有数据，有就直接加载。）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="comment"># 加载了两个数据集</span></span><br><span class="line">train_dataset = datasets.MNIST(root=<span class="string">&quot;./data&quot;</span>, train=<span class="literal">True</span>, transform=transform, download=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">test_dataset = datasets.MNIST(root=<span class="string">&quot;./data&quot;</span>, train=<span class="literal">False</span>, transform=transform, download=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">开始创建两个数据集的迭代器。64个批，训练集随机采样，测试集不随机。</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size = <span class="number">64</span>, shuffle=<span class="literal">True</span>)</span><br><span class="line">test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=<span class="number">64</span>, shuffle=<span class="literal">False</span>)</span><br></pre></td></tr></table></figure><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><p>举一个RNN的数据输入。</p><p>想要1000个样本，每个样本的序列长度为10， 输入维度input_size &#x3D; 5， 输出维度为2（<strong>每个样本一个输出标签</strong>）</p><p>参数解析：</p><ul><li>一个样本（序列段）<ul><li>seq_len（序列段长度，有多少个时间步）</li><li>input_size (输入特征维度)</li></ul></li><li>样本数量 1000</li><li><strong>批batch</strong>， 这个和<strong>迭代器</strong>绑定。</li></ul><p>torch的随机张量：</p><ul><li><code>torch.randn</code>(形状) 标准正态分布</li><li><code>torch.rand</code>(形状) [0, 1),均匀分布</li><li><code>torch.randint()</code>， 生成指定范围内的随机整数</li></ul><p><code>torch.utils.data.TensorDataset</code> 从<strong>tensor张量</strong>直接创建数据集，<strong>属于Dataset的一个子类</strong>。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch.utils.data <span class="keyword">import</span> DataLoader, TensorDataset</span><br><span class="line"></span><br><span class="line">num_samples = <span class="number">1000</span></span><br><span class="line">seq_len = <span class="number">10</span></span><br><span class="line">input_size = <span class="number">5</span></span><br><span class="line">output_size = <span class="number">2</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">X = torch.randn(num_samples, seq_len, input_size) <span class="comment"># 定义num_samples个样本。元素服从标准正态分布（大部分集中于0）</span></span><br><span class="line">Y = torch.randint(<span class="number">0</span>, output_size, size=(num_samples,)) <span class="comment"># 0表示随机数最小值，output_size 随机数最大值.[0,2)之间的整数，也就是0/1, size=指定尺寸，一维1000个。一个样本，对应一个标签值。一共1000个样本的标签</span></span><br><span class="line"></span><br><span class="line">dataset = TensorDataset(X,Y)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">把这1000个样本，随机切割成32个批</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">train_loader = DataLoader(dataset, batch_size = <span class="number">32</span>, shuffle=<span class="literal">True</span>)</span><br></pre></td></tr></table></figure><p>所以数据的逻辑是：</p><ol><li>元素<ol><li>（一个时间步）</li><li>图片的一个通道</li></ol></li><li><strong>样本</strong>（多个元素）<ol><li>多个时间步</li><li>3个通道的完整一张图片。（<strong>通道</strong>x 高度 x 宽度）（很像3个时间步）</li></ol></li><li><strong>批batch</strong>（多个样本）<ol><li>多个序列块</li><li>多张图片</li></ol></li></ol><h1 id="torch-模型"><a href="#torch-模型" class="headerlink" title="torch 模型"></a>torch 模型</h1><p>这里主要记录一下，各个主流模型的定义，以及，torch库里面集成的库，用于快速构建模型</p><h2 id="线性回归单元"><a href="#线性回归单元" class="headerlink" title="线性回归单元"></a>线性回归单元</h2><p>线性回归单元，就是我们之前说的<mark style="background:#fff88f">y &#x3D;wx + b</mark> (w, x 均为n维向量，b维标量) </p><p><img src="/images/406172634195b33b63e554bef3dd1840.jpg" alt="566"><br>所以<strong>一个线性回归单元</strong>，就有<strong>一个输出</strong>，对应<strong>一个w权重向量</strong>（n维），以及<strong>一个b偏置标量</strong>。</p><p>一般把网络横过来传导观看<br><img src="/images/Pasted%20image%2020260429150823.png" alt="112"></p><h2 id="全连接层"><a href="#全连接层" class="headerlink" title="全连接层"></a><em>全连接层</em></h2><p>上一节的线性回归单元，<strong>一个线性回归单元</strong>，对应：</p><ol><li>n维输入</li><li>1维输出</li><li>一个n维的w权重矩阵</li><li>一个1维的b偏置标量</li></ol><p>所以，当我们把他组合起来后，就形成了我们的全连接层</p><p><img src="/images/a1508a1ba46373bdde3eef56c46a7e6e.jpg"></p><p>所以当<strong>具有多层时</strong>，也就具有了从<strong>n维输入-&gt; m维输出</strong>的<strong>全连接层</strong></p><p>所以定义一个全连接层，仅需要确认：</p><ul><li>输入特征维数</li><li>输出特征维数</li></ul><hr><p>下面对<strong>两种用途的全连接层</strong>，做一个讨论</p><h3 id="回归"><a href="#回归" class="headerlink" title="回归"></a>回归</h3><p>以上的一个全连接层构成的<font color="#ff0000">FNN前馈神经网络</font>。这个模型输出的都是<font color="#ff0000">回归</font>，也就是<strong>预测一个值</strong>，比如价格，场次，天数（int, float这些）</p><p>这里，我们主要关注**<mark style="background:#fff88f">每个输出维度的值</mark>**。</p><h4 id="torch库定义层-初始化参数"><a href="#torch库定义层-初始化参数" class="headerlink" title="torch库定义层+初始化参数"></a>torch库定义层+初始化参数</h4><p><strong>torch库</strong>可以方便的帮我们提供一个全连接的对象，以及初始化权重矩阵，偏置矩阵的方法</p><ul><li><code>.weight.data</code> 用来访问权重矩阵<strong>W</strong></li><li><code>.bias.data</code> 用来访问偏置向量<strong>b</strong></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;定义一个输入维数为2， 输出维数为1的全连接层&quot;&quot;&quot;</span></span><br><span class="line">fc1 = nn.Linear(<span class="number">2</span>,<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;权重矩阵W.normal_正太分布，期望为0， 方差为0.01的随机初始化&quot;&quot;&quot;</span></span><br><span class="line">fc1.weight.data.normal_(<span class="number">0</span>, <span class="number">0.01</span>)</span><br><span class="line"><span class="string">&quot;&quot;&quot;偏置向量b.fill_全部0填充&quot;&quot;&quot;</span></span><br><span class="line">fc1.bias.data.fill_(<span class="number">0</span>)</span><br></pre></td></tr></table></figure><h4 id="torch-构造model对象"><a href="#torch-构造model对象" class="headerlink" title="torch 构造model对象"></a>torch 构造model对象</h4><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">from torch import nn</span></span><br><span class="line"><span class="string">nn就是torch里面的神经网络库文件</span></span><br><span class="line"><span class="string">torch.nn库， 提供了各种构造model对象的方法和类定义</span></span><br><span class="line"><span class="string">用全连接层组成一个网络&quot;&quot;&quot;</span></span><br><span class="line">model=nn.Sequential(fc1)</span><br></pre></td></tr></table></figure><h4 id="torch-创建损失函数对象"><a href="#torch-创建损失函数对象" class="headerlink" title="torch 创建损失函数对象"></a>torch 创建损失函数对象</h4><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">torch.nn库，提供了各种损失函数的类定义</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">loss=nn.MSELoss() <span class="comment">#就是均方误差：Mean Squared Error, MSE</span></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260429153432.png" alt="202"></p><h4 id="torch-优化器对象"><a href="#torch-优化器对象" class="headerlink" title="torch 优化器对象"></a>torch 优化器对象</h4><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">torch.optim库，提供了各种优化器的类定义</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">optim = torch.optim.SGD(model.parameters(), lr=<span class="number">0.01</span>)</span><br></pre></td></tr></table></figure><p>可以看到，我们每次用Loss来自动求导后，梯度分量全部存放在W，b这些模型参数对象的.grad属性里面。所以，优化器仅需要知道模型参数对象即可。<br><img src="/images/Pasted%20image%2020260429153921.png" alt="545"></p><blockquote><p>右边的平均梯度，就是我们反向传播自动求导的梯度，已经存放在参数对象里面了。</p></blockquote><h4 id="训练过程"><a href="#训练过程" class="headerlink" title="训练过程"></a>训练过程</h4><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">model.train() <span class="comment">#设置model对象，能够修改模型参数</span></span><br><span class="line"><span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">3</span>):</span><br><span class="line"><span class="keyword">for</span> X,y <span class="keyword">in</span> data_iter:</span><br><span class="line">l = loss(net(X), y) <span class="comment"># 使用损失函数对象的仿函数（预测值，标签值），固定好Loss损失函数的系数</span></span><br><span class="line">optim.zero_grad() <span class="comment"># 让优化器来清除模型参数对象中的梯度值</span></span><br><span class="line">l.backward() <span class="comment"># Loss(w,b)计算平均梯度，写入模型参数对象中</span></span><br><span class="line">optim.step() <span class="comment"># 利用学习率 + 平均梯度，更新模型参数。</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 该epoch训练完成，输入测试集，计算出当前的Loss(w,b)损失,正常情况下，每轮训练后，损失是会下降的。</span></span><br><span class="line">l = loss(model(features), labels)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;epoch %d, loss %4f&quot;</span> % (epoch, l))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">epoch 1, loss 0.000248</span></span><br><span class="line"><span class="string">epoch 2, loss 0.000103</span></span><br><span class="line"><span class="string">epoch 3, loss 0.000103</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><h3 id="分类（softmax回归）"><a href="#分类（softmax回归）" class="headerlink" title="分类（softmax回归）"></a>分类（softmax回归）</h3><p>但是，有时候，我们不关心具体的值，我们只关心是0&#x2F;1，也就是bool。也就是我们希望输出是一哪一个？是不是？</p><p>这个时候，我们关注的是：<mark style="background:#fff88f">输出哪个维度的位置</mark>， 或者<mark style="background:#fff88f">单个输出维度的二值性</mark></p><p>这两种关注角度都可以实现我们的分类需求：</p><ul><li>输出3个维度（猫，狗，人）进行分类</li><li>输出一个维度（输入1， 有人；输出0，没人）</li></ul><h4 id="分类问题中-标签的处理"><a href="#分类问题中-标签的处理" class="headerlink" title="分类问题中 标签的处理"></a>分类问题中 标签的处理</h4><p>因为是分类问题了，所以期望输出，要么关注输出的维度的位置，要么关注输出维度的二值性。</p><p>所以数据集中的标签，要做对应的one-hot编码，才能表示位置。<br><img src="/images/Pasted%20image%2020260429161417.png" alt="616"></p><h4 id="网络架构"><a href="#网络架构" class="headerlink" title="网络架构"></a>网络架构</h4><p>为了估计所有可能的类别的条件概率。我们肯定需要一个至少单层的模型，有多个输出，<strong>每个类别对应一个输出</strong>。</p><p>所以<strong>结构仍然是一个全连接层</strong>。</p><ul><li>输入维度4</li><li>输出维度3（分类3类）</li><li>权重矩阵（W）</li><li>偏置向量（b）<br><img src="/images/Pasted%20image%2020260429162514.png" alt="310"><br><img src="/images/Pasted%20image%2020260429162617.png" alt="129"></li></ul><p>所以softmax回归，本质上，并不是网络结构的变化，而是对输出内容的理解发生了变化，也就是数据集的标签发生了变化。</p><h4 id="softmax运算"><a href="#softmax运算" class="headerlink" title="softmax运算"></a>softmax运算</h4><p>现在我们的模型的每一个输出，都能回归出一个值，范围是随机一个数字, 因为每个输出的标签不是0，就是1.</p><p>这就会导致，三个输出回归出来的值可能是这样{0.7，0.4， 0.9}</p><p>光看这个值，看不出这个回归出来的每个类别的打分，有什么意义</p><ul><li>（不是概率，因为和不为1）</li><li>（每个输出，也可能回归出负数）</li></ul><p>所以，<strong>要将每个输出，改造成概率</strong>。要做到：</p><ul><li>每个类别，输出非负</li><li>总和为1</li></ul><p>所以，对输出，做一个<strong>softmax处理</strong>，<strong>全部去e的幂次方，然后取比例权重</strong>。<br><img src="/images/Pasted%20image%2020260429163349.png" alt="329"></p><p>然后选择最大的概率的那个，就是我们识别出来的分类。</p><p><img src="/images/e0610a65f2c46ef7fa66e1d0d4b58987.jpg" alt="425"></p><h4 id="交叉熵损失函数"><a href="#交叉熵损失函数" class="headerlink" title="交叉熵损失函数"></a>交叉熵损失函数</h4><p>前面在讲全连接层回归的时候，我们使用的是<strong>均方误差损失函数</strong>。</p><p>但是在这种分类问题中，我们一般不适用均方误差函数。因为我们设置损失函数，本质上就是希望，能去衡量出模型的优劣，具体说就是，能够反应预测值与标签值的差距大小嘛。</p><p>而我们的标签值是独热编码（1，0，0）这种。而我们的预测值，是softmax函数处理之后的每个分类的概率（0.7，0.2，0.1）这种。通过观察可以发现。</p><p>我们通过使用交叉熵损失函数，定义如下：<br><img src="/images/Pasted%20image%2020260430155259.png" alt="242"><br>这个是单个样本的损失，可以看到，yj是标签，yj’ 是预测的值，q是类别的数量。</p><p>所以，当我们<strong>输入一个样本之后</strong>，这个<font color="#ff0000">交叉熵损失函数</font>，就可以很好的衡量，这个<strong>预测的值准不准</strong>。<br><img src="/images/bc5f08e1c597558a63eeca363043e869.jpg" alt="379"></p><p>使用<strong>交叉熵损失函数</strong>，反向传播的时候，和多层回归最大的不同，就是，多了一个softmax环节嘛。</p><p>那么同样利用链式法则，把softmax()函数当成一个激活环节，把前面的输入作为预激活输出。这样<strong>只要知道Loss对预激活输出的梯度</strong>，这样就可以和前面的多层反向传播对上了[[#^e096f0]]</p><p>所以我们将<mark style="background:#fff88f">y &#x3D; softmax(o）</mark>代入<strong>loss</strong>，然后求导即可。</p><p><img src="/images/Pasted%20image%2020260430190334.png" alt="418"></p><p>以上就解释了单个样本的损失，当一个batch有多个样本的时候，我们通常就是所有样本的损失加起来，求平均，得到平均损失。</p><p><img src="/images/Pasted%20image%2020260430191253.png" alt="518"></p><h4 id="torch实现softmax回归"><a href="#torch实现softmax回归" class="headerlink" title="torch实现softmax回归"></a>torch实现softmax回归</h4><blockquote><p>如果我们要从零开始实现，也很简单，就是自己实现前向传播的公式就行了，torch构建出的计算图，就是为了实现数学公式，比较麻烦的是，要自己计算，记录，传播。</p></blockquote><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> torch.nn <span class="keyword">as</span> nn</span><br><span class="line"><span class="keyword">from</span> d21 <span class="keyword">import</span> torch <span class="keyword">as</span> d21</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;数据集&quot;&quot;&quot;</span></span><br><span class="line"><span class="comment"># 一批数据集是256个样本</span></span><br><span class="line">batch_size = <span class="number">256</span></span><br><span class="line"><span class="comment"># 准备好数据集的迭代器</span></span><br><span class="line">train_iter, test_iter = d21.load_data_fashion_mnist(batch_size)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;模型&quot;&quot;&quot;</span></span><br><span class="line"><span class="comment"># nn.Flatten()展平层，是把一张单通道图片样本（1x28x28）(通道x高度x宽度)压平</span></span><br><span class="line"><span class="comment"># 就一层FNN</span></span><br><span class="line">net = nn.Sequential(nn.Flatten(), nn.Linear(<span class="number">784</span>,<span class="number">10</span>))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始化模型参数</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_weights</span>(<span class="params">m</span>):</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">nn.init.normal_(m.weight, std=<span class="number">0.01</span>)</span><br><span class="line"></span><br><span class="line">net.apply(init_weights)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;损失函数对象</span></span><br><span class="line"><span class="string">pytorch里面，softmax()函数，通常不放在模型里面。而是集成在交叉熵损失函数里面。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">reduction=&#x27;mean&#x27;, 表示返回一个标量：所有样本的损失的平均值，求出来的就是平均梯度了。</span></span><br><span class="line"><span class="string">reduction=&#x27;sum&#x27;, 表示返回一个标量：所有样本的损失的和</span></span><br><span class="line"><span class="string">reduction=&#x27;none&#x27;, 返回每个样本的损失构成的张量</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">所以你可能还需要手动求平均，</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">loss = nn.CrossEntropyLoss(reduction=<span class="string">&#x27;none&#x27;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;创建优化器对象</span></span><br><span class="line"><span class="string">优化器对象，需要平均梯度</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">trainer = torch.optim.SGD(net.parameters(), lr=<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;训练&quot;&quot;&quot;</span></span><br><span class="line">num_epochs=<span class="number">10</span></span><br><span class="line">d21.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260430193558.png" alt="246"></p><p>有几点需要注意：</p><ul><li><p>pytorch里面，<strong><code>softmax</code> 通常不放在模型里面</strong>，而是集成在 <strong><code>nn.CrossEntropyLoss</code></strong> 损失函数中直接计算。</p><ul><li>原因：如果你先在模型里算 <code>softmax</code>，得到的概率 $\hat{y}_j$ 可能会因为非常小而被计算机四舍五入成 <strong>0</strong>（下溢）</li><li>当损失函数接着去计算 $\log(\hat{y}_j)$ 时，$\log(0)$ 会变成 **$-\infty$</li><li>从而使模型容易跑崩。</li></ul></li><li><p>pytorch里面，创建<strong>损失函数对象</strong>，loss&#x3D;nn.MSE()，然后使用的时候，是传入loss(预测值，标签值)，<strong>为什么不需要知道模型参数</strong>？</p><ul><li>当你执行 <code>预测值 = model(输入)</code> 时，PyTorch 会在后台悄悄建立一张<strong>计算图</strong></li><li>这个 <code>预测值</code> 张量内部包含了一个指向生成它的操作（比如线性层、激活函数）的指针，而这些操作又连接着模型的 <strong>权重（weight）</strong> 和 <strong>偏置（bias）</strong></li></ul></li></ul><h2 id="FNN-前馈"><a href="#FNN-前馈" class="headerlink" title="FNN 前馈"></a>FNN 前馈</h2><p>前面我们从最简单的线性回归单元-&gt;全连接层-&gt;单层&#x2F;多层全连接回归模型-&gt;softmax回归</p><p>前面重心放在理解，数据，模型，损失函数，梯度下降，softmax。</p><p>下面，我们来具体的研究模型这件事，首先第一个就是FNN，<strong>多个全连接层组成的神经网络</strong>，也叫<strong>多层感知机MLP</strong></p><h3 id="多层感知机MLP"><a href="#多层感知机MLP" class="headerlink" title="多层感知机MLP"></a>多层感知机MLP</h3><p>前面我们的一层全连接层， <code>z = wx + b</code>, 本质上都是<strong>线性的映射</strong>(<mark style="background:#fff88f">仿射变换</mark>)</p><p>但是，我们很难使用一层全连接的线性映射，来表示特征x-&gt; 标签y的映射。</p><p>所以，我们需要使用<strong>多层全连接层</strong>，也就是加入<mark style="background:#fff88f">隐藏层h1,h2,</mark>…</p><p>我们可以通过在网络中<strong>加入一个或多个隐藏层来克服线性模型的限制</strong>， 使其能<strong>处理更普遍的函数关系</strong>类型。 要做到这一点，最简单的方法是<strong>将许多全连接层堆叠在一起</strong>。 每一层都输出到上面的层，直到生成最后的输出</p><p>假设<strong>L层 全连接层</strong>，我们把<strong>前面的L-1层，看作表示</strong>，<strong>最后一层看作线性预测器</strong>。这种架构通常称为_<strong>多层感知机</strong><em>（multilayer perceptron），通常缩写为</em><strong>MLP_</strong></p><p><img src="/images/Pasted%20image%2020260430203406.png" alt="199"></p><p>如上图所示，输入4个维度，输出3个维度，中间隐藏层的单元5个维度。这个MLP的层数为2</p><p>然而，这种层层堆叠的MLP，它的参数开销很高。所以在不改变输入，输出大小的情况下，对模型的参数和有效性之间要衡量一下。</p><h3 id="激活函数"><a href="#激活函数" class="headerlink" title="激活函数"></a>激活函数</h3><p>前面我们提到的MLP的，他的各层全连接层，是首位相接的，但是，这会有一个问题：<br><mark style="background:#b1ffff">线性函数带入线性函数，还是线性</mark>，这回导致维数下降。</p><p>所以，为了发挥多层架构的潜力，我们还需要一个关键的连接因素：<br><mark style="background:#fff88f">在仿射变换之后，对每个隐藏单元，应用非线性的激活函数</mark>， <strong>激活函数的输出</strong>，被称为<strong>活性值</strong></p><p>有了激活函数，就不可能再将我们的多层感知机退化成线性模型<br><img src="/images/Pasted%20image%2020260430204132.png" alt="211"></p><p>全连接层输出（预激活值）<code>z = wx + b</code><br>隐藏层单元（活性值）<code>h=σ（z）</code><br>最后一层的线性预测器（输出）<code>o =wh + b</code></p><p>这样之后，模型才能充分发挥各层的潜力。</p><h4 id="ReLU函数"><a href="#ReLU函数" class="headerlink" title="ReLU函数"></a>ReLU函数</h4><p><strong>ReLU()函数</strong> ， 也叫<strong>修正线性单元</strong><br><img src="/images/Pasted%20image%2020260430204614.png" alt="171"><br><img src="/images/Pasted%20image%2020260430204640.png" alt="295"></p><p>ReLU(x)<br>当x &lt; 0, 导数为0， 当x &gt; 0, 导数为1， x&#x3D;0处的导数为0</p><p><mark style="background:#fff88f">ReLU函数的好处</mark>是：</p><ul><li><strong>求导表现特别好</strong>（反向传播计算梯度的时候）<ul><li>要么让参数消失，要么让参数通过</li></ul></li><li><strong>减轻了梯度消失问题</strong></li></ul><p>ReLU函数变体：</p><ul><li>参数化ReLU， pReLU<ul><li><img src="/images/Pasted%20image%2020260430205013.png" alt="232"></li></ul></li></ul><h4 id="sigmoid函数"><a href="#sigmoid函数" class="headerlink" title="sigmoid函数"></a>sigmoid函数</h4><p>sigmoid函数，将输入变换为区间（0，1）上的输出。</p><p><img src="/images/Pasted%20image%2020260430205414.png" alt="250"></p><p><img src="/images/Pasted%20image%2020260430205420.png" alt="295"></p><p>可以看到，sigmoid函数，是一个平滑，可导的阈值单元的近似。</p><p>当我们想要将输出看成是二元分类问题的概率时，就可以来用了。不过隐藏层用的较少</p><blockquote><p>可以看到，sigmoid函数，本质上就是想把任意x的取值，往0&#x2F;1上去近似映射。</p></blockquote><blockquote><p>RNN里面，利用sigmoid单元来控制时序信息流的架构。</p></blockquote><p>sigmoid函数，求导之后，可以看到，x&#x3D;0处，导数最大到0.25<br><img src="/images/Pasted%20image%2020260430205939.png" alt="267"></p><h4 id="tanh函数"><a href="#tanh函数" class="headerlink" title="tanh函数"></a>tanh函数</h4><p>tanh函数，和sigmoid函数类似，不过他映射的范围不是（0，1），而是（-1，1）</p><p><img src="/images/Pasted%20image%2020260430205829.png"><br><img src="/images/Pasted%20image%2020260430205836.png" alt="322"></p><p>这个tanh函数求导后，x&#x3D;0处取导数最大值1<br><img src="/images/Pasted%20image%2020260430210025.png" alt="243"></p><h3 id="torch实现MLP"><a href="#torch实现MLP" class="headerlink" title="torch实现MLP"></a>torch实现MLP</h3><p>下面用torch库来实现多层感知机，先来定义一下模型，并初始化模型参数‘</p><p>pytorch支持<strong>自动挡</strong> 创建模型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">d2l是 动手学深度学习的 辅助库，就是个教学工具包</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">from</span> d21 <span class="keyword">import</span> torch <span class="keyword">as</span> d21</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;模型&quot;&quot;&quot;</span></span><br><span class="line">net = nn.Sequential(nn.Flatten(), <span class="comment"># 多加一个这个是为了读取图片</span></span><br><span class="line">nn.Linear(<span class="number">784</span>,<span class="number">256</span>),</span><br><span class="line">nn.ReLU(),</span><br><span class="line">nn.Linear(<span class="number">256</span>,<span class="number">10</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_weights</span>(<span class="params">m</span>):</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">nn.init_normal_(m.weight, std=<span class="number">0.01</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># net.apply 里面定义一个针对层的巡检规则，如果是线性层，就初始化参数为正态分布</span></span><br><span class="line">net.apply(init_weights)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>也支持<strong>手动挡</strong> 创建模型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyModel</span>(nn.Module):</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line"><span class="built_in">super</span>().__init__()</span><br><span class="line"><span class="variable language_">self</span>.fc = nn.Linear(<span class="number">784</span>, <span class="number">256</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 直接初始化</span></span><br><span class="line">nn.init.normal_(<span class="variable language_">self</span>.fc.weight, std=<span class="number">0.01</span>)</span><br><span class="line">nn.init.constant_(<span class="variable language_">self</span>.fc.bias, <span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x</span>):</span><br><span class="line"><span class="comment"># ...</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>以上使用<code>nn.init.normal_(self.fc.weight, std = 0.01)</code> 是手动挡实现层的参数初始化。</p><p>也可以使用<strong>自动初始化</strong>，<strong>通过根据层的输入输出神经元数量</strong>，来自动调整初始化范围, 这样就<strong>不用自己指定标准差了</strong></p><ul><li><strong>Xavier 初始化</strong>（适用于 Sigmoid&#x2F;Tanh）：<code>nn.init.xavier_normal_(m.weight)</code></li><li><strong>Kaiming 初始化</strong>（适用于 ReLU）：<code>nn.init.kaiming_normal_(m.weight)</code></li></ul><p>定义好了模型，下面开始获取数据，定义损失函数，优化器，然后训练</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">batch_size, lr, num_epochs = <span class="number">256</span>, <span class="number">0.1</span>, <span class="number">10</span></span><br><span class="line">loss = nn.CrossEntropyLoss(reduction=<span class="string">&#x27;none&#x27;</span>)</span><br><span class="line">trainer = torch.optim.SGD(net.parameters(), lr=lr)</span><br><span class="line"></span><br><span class="line">train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)</span><br><span class="line">d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)</span><br></pre></td></tr></table></figure><h3 id="模型选择，欠拟合，过拟合"><a href="#模型选择，欠拟合，过拟合" class="headerlink" title="模型选择，欠拟合，过拟合"></a>模型选择，欠拟合，过拟合</h3><p>我们希望模型，能够真正发现一种泛化模式，而不是简单的记住数据。所以希望有泛化能力。</p><blockquote><p>损失，是针对单个样本来说的；误差，是针对整个数据集的宏观表现，比如RMSE</p></blockquote><p><mark style="background:#affad1">误差分类</mark>：</p><ul><li><strong>训练误差</strong>：模型在训练集上的loss损失</li><li><strong>泛化误差</strong>：模型应用在从原始样本分布中抽取的无限多数据样本时，误差的期望。我们在测试集上算出的误差，本质上是对泛化误差的一种估计</li></ul><p><mark style="background:#affad1">几个影响模型泛化的因素</mark>：</p><ol><li>可调整<strong>参数的数量</strong>。当可调整参数的数量（有时称为_自由度_）很大时，模型往往更容易过拟合。</li><li><strong>参数采用的值</strong>。当权重的取值范围较大时，模型可能更容易过拟合。</li><li><strong>训练样本的数量</strong>。即使模型很简单，也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。</li></ol><p><mark style="background:#affad1">把数据集分成3块</mark>：</p><ul><li>训练集：来训练模型（课后作业）</li><li>验证集：用来选择模型（模拟考）</li><li>测试集：用来得出最终选择的模型的性能（高考）</li></ul><p><mark style="background:#affad1">K折交叉验证</mark><br>当训练数据稀缺时，把训练数据分成K个子集。然后<strong>执行K次训练和验证</strong>：<strong>每次在K-1个自己上训练，并在剩余一个子集上验证</strong>。最后结果取平均来估计训练误差和验证误差</p><p><mark style="background:#affad1">模型泛化性能</mark><br><img src="/images/Pasted%20image%2020260430220121.png" alt="307"><br><strong>过拟合</strong>：训练集上效果好，但是测试集效果不好</p><ul><li>解决办法：<ul><li><mark style="background:#fff88f">正则化</mark></li><li>增加数据量<br><strong>欠拟合</strong>：训练集效果不好，测试集效果也不好</li></ul></li><li>解决办法：<ul><li>增加模型复杂度</li><li>输入更多维度的数据</li><li>增加训练轮次</li><li>减少正则化</li></ul></li></ul><h3 id="模型复杂度"><a href="#模型复杂度" class="headerlink" title="模型复杂度"></a>模型复杂度</h3><p>前面我们讲到，如果模型出现过拟合，说明模型太复杂了，需要使用正则化技术。因为我们不太可能收集更多的训练数据。</p><p>假设我们已经有了尽可能多的高质量数据。重点便放在正则化上</p><p>假设模型输入维度（x1,x2, ……, xn）。那么经过一层全连接后，得到的输出，是关于输入的一阶多项式。</p><p>当经过多层之后，<strong>模型拟合的多项式的阶数 &#x3D; 激活函数的个数</strong>， 这只是抽象看，实际上不是多项式，而是分段线性<br><img src="/images/Pasted%20image%2020260501092615.png" alt="608"></p><p>我们先来<mark style="background:#affad1">从多项式的角度理解模型复杂度</mark><br>理解前提：我们的整个模型，本质上，就是一个多项式函数。就是一个巨大的函数嘛<br><img src="/images/cd235e6c9345a6ff99a936afc026979e.jpg" alt="510"></p><p>所以可以看到，模型复杂度和两件事有关：</p><ol><li>模型层数d</li><li>输入特征维度k</li></ol><p>这里，要认识到，<strong>模型复杂度，其实是一个动态变化的东西</strong>：</p><ul><li>模型阶数d不变，导致模型多项式的阶数肯定是固定的</li><li>输入特征的维度k，决定了，每一阶特征的种数，理论上你有 $C_{k-1+d}^{k-1}$ 种特征。</li></ul><p>但是在具体训练过程中，我们会调整模型参数，也就是多项式每一项特征的系数，所以并不是所有理论的特征都会出现，因此，<mark style="background:#fff88f">模型的复杂度，其实是在动态变化的</mark>。</p><h4 id="权重衰退（L2正则化）"><a href="#权重衰退（L2正则化）" class="headerlink" title="权重衰退（L2正则化）"></a>权重衰退（L2正则化）</h4><p>当理解了模型复杂度之后，我们要知道：</p><ul><li>仅仅通过限制阶数d，模型也可能复杂</li></ul><p>我们需要一个更细粒度的工具来调整模型函数的复杂度。</p><p>所以，我们的权重衰退，本质上，就是为了降低模型复杂度。（图像上体现在平滑一点，降维了）</p><p><mark style="background:#fff88f">L2正则化</mark>：<strong>通过函数与0的距离，来衡量函数的复杂度</strong>。<br>那么如何衡量函数f和0的距离呢？其实没有唯一标准</p><p><strong>现在的情况是</strong>：</p><ul><li>我们的<strong>目的</strong>：<strong>降低模型复杂度，来减少过拟合</strong></li><li><strong>如何降低模型复杂度呢</strong>？<ul><li>模型阶数d，改变不了</li><li>输入特征维数k，每一阶特征理论总数 $C_{k-1+d}^{k-1}$ 种特征，也变不了</li><li><mark style="background:#fff88f">只能通过修改特征的系数（模型参数），来减少特征的出现。</mark></li></ul></li></ul><p>我们<strong>就要在训练中，增加一个功能，来让模型参数w, 也就是特征系数，尽可能的-&gt;0， 这样可以有效减少出现特征的数量</strong>。</p><p>那么如何在训练中增加这个功能呢？</p><p><mark style="background:#fff88f">答案</mark>：在损失函数Loss中（<strong>训练的目的，是让Loss-&gt;0</strong>），<strong>加入一个惩罚项$|\mathbf{w}|^2$,  这样模型在优化过程中不仅要减小预测误差，还要保证权重向量尽可能小</strong></p><p>好处是：<br>权重w 均匀整体的变小调整，不是简单的丢弃某个特征。同时整体上，也限制了个别特征独大。</p><blockquote><p>如果没有正则化，模型可能会为了拟合某个噪声点，给某个高阶特征分配一个极大的权重，导致拟合曲线剧烈抖动。$L_2$ 正则化强迫系数保持在一个较小的范围，使得函数更加平滑</p></blockquote><p>这个惩罚项，就是我们的L2的范数：<br><img src="/images/Pasted%20image%2020260501101734.png" alt="636"></p><p>因此，使用L2正则化，来改造我们的损失函数</p><p>这是原来的平均损失函数（一个batch的样本）<br><img src="/images/Pasted%20image%2020260501101827.png" alt="322"><br><img src="/images/Pasted%20image%2020260501101913.png" alt="214"></p><p><strong>加入的λ，为正则化常数（&gt;0）</strong></p><p><mark style="background:#d3f8b6">为什么我们选择L2范数，而不是L1范数？</mark></p><ul><li>L2正则化线性模型构成经典的<strong>岭回归算法</strong><ul><li>使用L2范数的一个<strong>原因</strong>是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法<strong>偏向于在大量特征上均匀分布权重</strong>的模型。 </li><li>在实践中，这可能使它们对<strong>单个变量中的观测误差更为稳定</strong></li></ul></li><li>L1正则化线性模型是统计学中类似的基本模型，为<strong>套索回归</strong>。<ul><li>L1惩罚会导致模型将权重集中在一小部分特征上， 而将其他权重清除为零。 这称为_<strong>特征选择</strong>_（feature selection），这可能是其他场景下需要的</li></ul></li></ul><p>既然已经正则化改造了我们的损失函数，下面就是梯度下降，优化器更新模型参数了。一个batch的更细如下：<br><img src="/images/Pasted%20image%2020260501102427.png" alt="458"></p><p>可以看到，具体的优化操作，：</p><ul><li>不仅<strong>根据估计值和标签值之间的差异</strong>来更新（后一项）</li><li>还<strong>根据权重本身的大小</strong>来更新</li></ul><p>这也就是为什么叫做<mark style="background:#fff88f">权重衰退</mark>，权重衰减<strong>为我们提供了一种连续的机制来调整函数的复杂度</strong></p><blockquote><p>λ越小，对w的约束越小，正则化效果越小；<br>λ越大，对w的约束越大，图像越平滑</p></blockquote><p>至于对偏置的惩罚，不同层不一样。最后一层网络输出层，不用正则化偏置。</p><h4 id="dropout-暂退法"><a href="#dropout-暂退法" class="headerlink" title="dropout 暂退法"></a>dropout 暂退法</h4><p>前面理解了L2正则化，现在换一个角度，从概率的角度看</p><p>我们假设一个先验，权重的值均取自均值为0的高斯分布。同时我们也期望模型深度挖掘特征，<strong>将权重分散到许多特征中，而不是过于依赖少数潜在的虚假关联</strong>。（特征系数，尽量平均高斯，不能就靠几个特征）</p><ul><li>当面对<strong>更多的特征</strong>而<strong>样本不足</strong>时，线性模型往往会<strong>过拟合</strong>。 </li><li>相反，当给出<strong>更多样本</strong>而不是特征，通常线性模型<strong>不会过拟合</strong></li></ul><p>所以这表明 <mark style="background:#fff88f">特征，样本，过拟合之间的关系</mark>：</p><ul><li>特征：决定模型复杂度的上限<ul><li>阶数d越高，输入维度k越大，模型生成的特征项就越多，模型也就越“复杂”</li><li>如果<strong>特征数量远超样本量</strong>，线性模型往往会通过给每个特征指定过大的权重，精准地“<strong>背下”所有训练数据的细节</strong>（包括噪声），从而导致过拟合</li></ul></li><li>样本：决定模型的“泛化底气”<ul><li><strong>缓解过拟合</strong>：当样本数量相对于特征数量非常充裕时，线性模型通常不会过拟合。</li><li><strong>泛化的代价</strong>：然而，仅仅依靠增加样本而保持<strong>模型简单</strong>（如简单的线性模型）是有代价的：这种模型可能<strong>无法考虑到特征之间复杂的交互作用</strong>，导致其捕捉信息的能力较弱</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260501103848.png" alt="551"></p><p><mark style="background:#fff88f">泛化性-灵活性的权衡：<em>偏差-方差权衡</em></mark></p><ul><li><strong>线性模型</strong><ul><li><strong>有很高的偏差</strong>：它们只能表示一小类函数。</li><li>然而，这些模型的<strong>方差很低</strong>：它们在不同的随机数据样本上可以得出相似的结果</li></ul></li><li><strong>深度神经网络</strong><ul><li>位于偏差-方差谱的另一端，与线性模型不同，神经网络并不局限于单独查看每个特征，而是学习特征之间的交互</li></ul></li></ul><p><mark style="background:#fff88f">即使我们有比特征多得多的样本，深度神经网络也有可能过拟合</mark></p><p><mark style="background:#affad1">什么是<strong>好的预测模型</strong>？</mark></p><ul><li><strong>泛化性</strong>：在未知数据上有好的表现，模型相对简单</li><li><strong>平滑性</strong>：不应该对输入的微小变化敏感，加不加噪声基本不能影响。<ul><li>提出了一个想法：<strong>在训练过程中，他们建议在计算后续层之前向网络的每一层注入噪声。 因为当训练一个有多层的深层网络时，注入噪声只会在输入-输出映射上增强平滑性</strong></li></ul></li></ul><p>这个想法就被称为<mark style="background:#fff88f">暂退法</mark><br> <strong>暂退法</strong>在<strong>前向传播过程中</strong>，<strong>计算每一内部层</strong>的<strong>同时注入噪声</strong>(实际操作时通过丢弃神经元来加噪声的)，这已经成为训练神经网络的常用技术。 这种方法之所以被称为_<strong>暂退法</strong>_，<br> <br> &gt; 因为我们<strong>从表面上看是在训练过程中丢弃（drop out）一些神经元</strong>。 在整个训练过程的每一次迭代中，标准暂退法包括在计算下一层之前将当前层中的一些节点置零</p><p><mark style="background:#affad1">如何注入这种噪声</mark>。<br><em>无偏向</em>（unbiased）的方式注入噪声</p><ul><li>第一种方法：<ul><li><strong>高斯噪声（添加式）</strong>：这是毕晓普（Bishop）提出的方法，通过将均值为零的高斯噪声 $\epsilon \sim \mathcal{N}(0, \sigma^2)$ 添加到输入 $\mathbf{x}$ 中，产生扰动点 $\mathbf{x}’ &#x3D; \mathbf{x} + \epsilon$。其期望值 $E[\mathbf{x}’] &#x3D; \mathbf{x}$ 保持不变。<ul><li><img src="/images/Pasted%20image%2020260501105043.png"></li></ul></li><li><strong>标准暂退法（丢弃式）</strong>：这是目前深度学习中最常用的方法。它不是添加高斯分布的数值，而是使用<strong>随机变量</strong>按概率 $p$ 直接“丢弃”节点<ul><li><img src="/images/Pasted%20image%2020260501105122.png"></li></ul></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260501105510.png"><br><img src="/images/Pasted%20image%2020260501105441.png" alt="144"></p><p>pytorch简洁实现</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">dropout1=<span class="number">0.2</span></span><br><span class="line">dropout2=<span class="number">0.5</span></span><br><span class="line">net = nn.Sequential(</span><br><span class="line">nn.Flatten(),</span><br><span class="line">nn.Linear(<span class="number">784</span>, <span class="number">256</span>),</span><br><span class="line">nn.ReLU(),</span><br><span class="line">nn.Dropout(dropout1),</span><br><span class="line">nn.Linear(<span class="number">256</span>, <span class="number">256</span>),</span><br><span class="line">nn.ReLU(),</span><br><span class="line">nn.Dropout(dropout2),</span><br><span class="line">nn.Linear(<span class="number">256</span>,<span class="number">10</span>)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_weights</span>(<span class="params">m</span>):</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">nn.init.normal_(m.weight, std=<span class="number">0.01</span>)</span><br><span class="line"></span><br><span class="line">net.apply(init_weights)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="推理-训练的计算图"><a href="#推理-训练的计算图" class="headerlink" title="推理&#x2F;训练的计算图"></a>推理&#x2F;训练的计算图</h3><p>这里主要开始具体理解，前向传播（推理），反向传播（训练）的计算图了</p><p>我们讨论，L2正则化的单隐藏层的MLP的网络模型上</p><p>我们先罗列好前向传播的计算<br><img src="/images/Pasted%20image%2020260501111448.png" alt="342"></p><p>这个就是一步步推导，没什么好说的，其pytorch底层构建的前向传播的计算图如下：<br><img src="/images/Pasted%20image%2020260501111551.png"></p><p><mark style="background:#affad1">反向传播</mark><br><img src="/images/Pasted%20image%2020260501111952.png" alt="554"><br><img src="/images/Pasted%20image%2020260501112005.png" alt="569"></p><p>这个在讨论[[#多层FNN时链式法则，反向传播更新梯度的过程]]的时候，已经推导过了。很简单。</p><h3 id="数值稳定性和模型初始化"><a href="#数值稳定性和模型初始化" class="headerlink" title="数值稳定性和模型初始化"></a>数值稳定性和模型初始化</h3><p>前面，我们讲解了推理，训练的计算图的过程。我们计算梯度，时刻都要用到当前的模型参数，和模型数据。</p><p>所以，这就带来了两个问题，一个是稳定性，一个是模型参数的初始化。</p><p><strong>初始化方案的选择</strong>在神经网络学习中起着举足轻重的作用， 它对保持数值稳定性至关重要<br>这些<strong>初始化方案的选择</strong>可以与<strong>非线性激活函数</strong>的选择有趣的结合在一起</p><p> 我们选择<font color="#ff0000">哪个激活函数</font>以及如何<font color="#ff0000">初始化参数</font>可以决定优化算法<strong>收敛的速度</strong>有多快</p><p>糟糕选择可能会导致我们在<strong>训练时遇到梯度爆炸或梯度消失</strong></p><p>所以<mark style="background:#fff88f">梯度消失，梯度爆炸，是指的训练有效无效这件事情</mark>。</p><p>我们现在假设一个L层，输入x, 输出o的深层网络。每一层的变换为fl, 那么可以<strong>直接写出o关于任意一层的参数的梯度。</strong></p><p><img src="/images/Pasted%20image%2020260501140836.png" alt="629"></p><p>可以看到，和我们[[#多层FNN时链式法则，反向传播更新梯度的过程]]里面推到的一样，你看最后一项，就是第l层的局部梯度，前面是第l层向后的几层的中间梯度。</p><p>这个会带来一件事，训练（反向传播的时候），值是从后往前传导的。这会导致一个问题：</p><p><strong>我们容易受到数值下溢问题的影响. 当将太多的概率乘在一起时，这些问题经常会出现</strong></p><p><mark style="background:#fff88f">前面从M(L)…..M(l+1), 这几项，他们的乘积，可能非常大，也可能非常小</mark></p><p><mark style="background:#affad1">梯度的“乘法效应”</mark><br>浅层网络的梯度计算依赖于深层网络</p><ul><li><strong>数学对应</strong>：在公式 (4.8.2) 中，计算第 $l$ 层的梯度 $\partial_{\mathbf{W}^{(l)}}\mathbf{o}$ 时，需要乘以 $L-l$ 个矩阵 $\mathbf{M}^{(L)} \cdot \ldots \cdot \mathbf{M}^{(l+1)}$</li><li><strong>层数越多，项越多</strong>：网络越深（$L$ 越大），这个连乘的项就越多，不稳定性也就被成倍放大</li></ul><p>这就面临一些问题：</p><ul><li><mark style="background:#fff88f"><em>梯度爆炸</em>（gradient exploding）</mark>问题： 参数更新过大，破坏了模型的稳定收敛<ul><li>当矩阵的特征值很大时，乘积会变得非常大。这会导致<strong>参数更新过大</strong>，从而破坏模型的稳定收敛，模型可能直接“跑飞”了，无法找到最优解</li></ul></li><li><mark style="background:#fff88f"><em>梯度消失</em>（gradient vanishing）</mark>问题： 参数更新过小，在每次更新时几乎不会移动，导致模型无法学习<ul><li>这通常是因为矩阵特征值很小，导致连乘结果发生<strong>数值下溢</strong>。后果是<strong>参数更新过小</strong>，模型在每次更新时几乎不动，导致模型完全无法学习知识，就像学生听课完全听不进去一样</li></ul></li></ul><h4 id="稳定性问题"><a href="#稳定性问题" class="headerlink" title="稳定性问题"></a>稳定性问题</h4><h5 id="梯度消失"><a href="#梯度消失" class="headerlink" title="梯度消失"></a>梯度消失</h5><p>我们针对sigmoid激活函数来看看，他容易导致梯度消失<br><img src="/images/Pasted%20image%2020260501142144.png" alt="290"></p><p>当输入很大&#x2F;很小的时候，梯度就会消失了。所以当你反向传播的时候，如果输入很大的地方，你的梯度就是0。导致梯度消失。所以一般选择<strong>ReLU函数</strong></p><h5 id="梯度爆炸"><a href="#梯度爆炸" class="headerlink" title="梯度爆炸"></a>梯度爆炸</h5><p>简单举例就是，深层的梯度可能还算正常，但是经过100层，浅层的梯度就会变多非常大<br><img src="/images/Pasted%20image%2020260501142419.png" alt="488"></p><h5 id="模型对称性问题"><a href="#模型对称性问题" class="headerlink" title="模型对称性问题"></a>模型对称性问题</h5><p>假如一个MLP，有一个隐藏层，两个隐藏单元，这个时候，每一层的隐藏单元之间没有什么区别。都是对称的</p><p>如果我们将隐藏层的参数全部初始化为一个常量，这个时候，这个隐藏层就好像只有一个单元。</p><p>解决办法：<mark style="background:#fff88f">dropout（暂退法正则化）</mark></p><h4 id="参数初始化"><a href="#参数初始化" class="headerlink" title="参数初始化"></a>参数初始化</h4><p>前面我们默认使用自己写的init_weights()来初始化Linear层，使用的是正态分布来初始化权重参数。</p><p>如果不指定初始化方法， pytorch框架会使用默认的随机初始化方法</p><h5 id="Xavier初始化"><a href="#Xavier初始化" class="headerlink" title="Xavier初始化"></a>Xavier初始化</h5><p>对于一个全连接层<br><img src="/images/Pasted%20image%2020260501143248.png" alt="99"></p><p>权重矩阵W的参数，都是从同一分布种独立抽取的。假设他具有0均值，σ2的方差（不一定是高斯）</p><p>此时假设输入x张量，也是0均值，γ2方差。且和W独立</p><p>那么输出o张量的均值和方差计算如下：<br><img src="/images/Pasted%20image%2020260501143450.png" alt="265"></p><p>所以保持输出方差不变的方法是设置nσ2 &#x3D; 1。（前向传播）<br>当反向传播算梯度的时候，也是要nσ2&#x3D;1，否则梯度的方差也会增大。</p><p>我们只要<img src="/images/Pasted%20image%2020260501143748.png"></p><p>这个就是Xavier初始化的基础。<br><img src="/images/Pasted%20image%2020260501143832.png"></p><p>它可以让前向，反向传播的时候，方差不会增大？</p><p>没错，老师，您的总结一针见血！<strong>Xavier 初始化的核心目标就是让每一层输出的方差和每一层梯度的方差都保持稳定</strong>，从而解决您之前担心的梯度消失和梯度爆炸问题</p><p><img src="/images/Pasted%20image%2020260501144120.png"></p><h3 id="分布偏移"><a href="#分布偏移" class="headerlink" title="分布偏移"></a>分布偏移</h3><p>这一节，主要讲的是，你的训练集，和你的测试集，分布不一样的情况</p><h4 id="类别"><a href="#类别" class="headerlink" title="类别"></a>类别</h4><p><mark style="background:#affad1">协变量偏移</mark><br>这是最主要的，训练集和测试集，分布不一样<br>训练集用的20岁的样本，测试集用的是60岁的样本</p><blockquote><p>训练集由真实照片组成，而测试集只包含卡通图片。 假设在一个与测试集的特征有着本质不同的数据集上进行训练， 如果没有方法来适应新的领域<br><img src="/images/Pasted%20image%2020260501152606.png" alt="444"></p></blockquote><p><mark style="background:#affad1">标签偏移</mark><br><img src="/images/Pasted%20image%2020260501152453.png" alt="518"></p><p><mark style="background:#affad1">概念偏移</mark><br><img src="/images/Pasted%20image%2020260501152833.png" alt="505"></p><p><img src="/images/Pasted%20image%2020260501153057.png"></p><h4 id="纠正偏差"><a href="#纠正偏差" class="headerlink" title="纠正偏差"></a>纠正偏差</h4><p>纠正偏移的核心思想其实只有一句话：<strong>既然训练集和测试集的题目分布不一样，那我们就给那些在测试集里更常出现的题目“加分”（提高权重），让模型多重视它们</strong></p><p><mark style="background:#affad1"> 1. 经验风险 vs 实际风险</mark></p><ul><li><p><strong>经验风险 (Empirical Risk)</strong>：就是模型在<strong>训练集</strong>上的平均损失。我们平时的训练目标就是让它最小化。</p></li><li><p><strong>实际风险 (True Risk)</strong>：是模型在真实世界（全量数据）分布下的损失。</p></li><li><p><strong>纠正的目标</strong>：因为训练集分布 $q(x)$ 和真实测试分布 $p(x)$ 不一样，我们要通过数学变换，把训练时的目标调整到接近真实风险。</p></li></ul><p><mark style="background:#affad1"> 2. 协变量偏移纠正：重新衡量样本权重</mark></p><p>当你发现受试者从年轻人（训练集 $q(x)$）变成了老人（测试集 $p(x)$）时，纠正的方法是引入一个<strong>权重系数 $\beta_i$</strong>：</p><p>$$\beta_i &#x3D; \frac{p(x_i)}{q(x_i)}$$</p><ul><li><p><strong>直观理解</strong>：如果一个步态特征 $x$ 在老人的数据里出现的概率很高 ($p(x)$ 大)，但在学生的数据里很少见 ($q(x)$ 小)，那么 $\beta$ 就会很大。</p></li><li><p><strong>做法</strong>：<mark style="background:#fff88f">在算损失函数时，不是简单的求平均，而是给每个样本乘上这个 $\beta$。</mark></p></li><li><p><strong>通俗点说</strong>：这就是在告诉模型：“虽然你在学生数据里只见过几次这种蹒跚的步态，但它在实战中非常重要，你得加倍重视它！”</p></li></ul><p><mark style="background:#affad1">3.  标签偏移纠正：按类别比例“加餐”</mark></p><p>这在分类任务中很常见。比如训练集里“平地”占 $90%$，但测试集里“上坡”占 $70%$。</p><ul><li><p><strong>权重 $\beta_i$</strong>：变成了标签概率的比率 $\frac{p(y_i)}{q(y_i)}$。</p></li><li><p><strong>纠正逻辑</strong>：既然“上坡”成了测试集的主角，那就在训练时，人为地放大那些标签为“上坡”的样本的损失。</p></li><li><p><strong>优势</strong>：由于标签 $y$ 通常维度很低（比如只有 3 个动作类别），估算这个比率比估算复杂的传感器信号分布 $x$ 要容易得多。</p></li></ul><p><mark style="background:#affad1">4. 概念偏移纠正：没有捷径，只能“与时俱进”</mark></p><p>这是最棘手的。因为 $P(y|x)$ 变了（同样的姿态，因为疲劳，所需的力矩变了）。</p><ul><li><p><strong>公式没用了</strong>：这种情况下，之前的公式无法直接纠正，因为连“标准答案”都漂移了。</p></li><li><p><strong>解决办法</strong>：</p><ul><li><p><strong>持续学习</strong>：不断用新产生的数据微调模型。</p></li><li><p><strong>衰减旧数据</strong>：让模型慢慢“忘掉”很久以前的状态，只学近期的规律。</p></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260501154006.png" alt="578"></p><h2 id="torch-构建模型"><a href="#torch-构建模型" class="headerlink" title="torch 构建模型"></a>torch 构建模型</h2><h3 id="块构建"><a href="#块构建" class="headerlink" title="块构建"></a>块构建</h3><p>这一节就是主要完成torch 如何构建模型的。<br>一个模型的构成：</p><ul><li>单元</li><li>层</li><li>块</li><li>组件</li></ul><p>为了实现这些复杂的网络，我们引入了<strong>神经网络块</strong>的概念。 <em>块</em>（block）可以描述单个层、由多个层组成的组件或整个模型本身</p><p>注意,python创建类对象的语法如下。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">对象名 = 类名(参数<span class="number">1</span>, 参数<span class="number">2</span>, ...)</span><br><span class="line"></span><br><span class="line"><span class="comment">//当你执行这行代码时，Python 会在后台做两件事：</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//1. 在内存中为这个对象开辟空间。  </span></span><br><span class="line"><span class="comment">//2. 自动调用类里面的 `__init__` 方法（构造函数），把参数传进去，完成对象的初始化。</span></span><br></pre></td></tr></table></figure><p><mark style="background:#affad1"><code>nn.Sequential()</code></mark></p><p>nn.Sequential定义了一个特殊的Module, 表示为一个<strong>块的类</strong>。它重写了 <code>__call__</code> 方法。所以你在代码最后写 <code>net(X)</code> 时，它会自动按照你定义的顺序，把数据 <code>X</code> 依次传给第一层、第二层……直到输出结果</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> torch.nn <span class="keyword">import</span> functional <span class="keyword">as</span> F</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(nn.Linear(<span class="number">20</span>, <span class="number">256</span>), nn.ReLU(), nn.Linear(<span class="number">256</span>, <span class="number">10</span>))</span><br><span class="line"></span><br><span class="line">X = torch.rand(<span class="number">2</span>, <span class="number">20</span>) <span class="comment"># shape（2，20）</span></span><br><span class="line">net(X)</span><br></pre></td></tr></table></figure><p>上面的 nn.Linear(20, 256), 返回的是Linear类的实例，<code>Linear</code>类本身就是<code>Module</code>的子类</p><p>通过<code>net(X)</code>调用我们的模型来获得模型的输出。 这实际上是<code>net.__call__(X)</code>的简写</p><p>Sequential类的自己实现：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MySequential</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, *args</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="keyword">for</span> idx, module <span class="keyword">in</span> <span class="built_in">enumerate</span>(args):</span><br><span class="line">            <span class="comment"># 这里，module是Module子类的一个实例。我们把它保存在&#x27;Module&#x27;类的成员</span></span><br><span class="line">            <span class="comment"># 变量_modules中。_module的类型是OrderedDict</span></span><br><span class="line">            <span class="variable language_">self</span>._modules[<span class="built_in">str</span>(idx)] = module</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        <span class="comment"># OrderedDict保证了按照成员添加的顺序遍历它们</span></span><br><span class="line">        <span class="keyword">for</span> block <span class="keyword">in</span> <span class="variable language_">self</span>._modules.values():</span><br><span class="line">            X = block(X)</span><br><span class="line">        <span class="keyword">return</span> X</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">自定义块</mark><br>前面使用自动定义的顺序块。我们也可以自己手动实现块</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MLP</span>(nn.Module):</span><br><span class="line">    <span class="comment"># 用模型参数声明层。这里，我们声明两个全连接的层</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="comment"># 调用MLP的父类Module的构造函数来执行必要的初始化。</span></span><br><span class="line">        <span class="comment"># 这样，在类实例化时也可以指定其他函数参数，例如模型参数params（稍后将介绍）</span></span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.hidden = nn.Linear(<span class="number">20</span>, <span class="number">256</span>)  <span class="comment"># 隐藏层</span></span><br><span class="line">        <span class="variable language_">self</span>.out = nn.Linear(<span class="number">256</span>, <span class="number">10</span>)  <span class="comment"># 输出层</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义模型的前向传播，即如何根据输入X返回所需的模型输出</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        <span class="comment"># 注意，这里我们使用ReLU的函数版本，其在nn.functional模块中定义。</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.out(F.relu(<span class="variable language_">self</span>.hidden(X)))</span><br><span class="line">        </span><br><span class="line">        </span><br><span class="line">net = MLP() <span class="comment"># 如果我不初始化，系统会自动初始化</span></span><br><span class="line">net(X)</span><br></pre></td></tr></table></figure><p>自定义块的时候，可以在forward()中，<mark style="background:#fff88f">自定义前向传播的程序控制流</mark> 。</p><p>这个仅展示可以随意指定计算流</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">FixedHiddenMLP</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="comment"># 不计算梯度的随机权重参数。因此其在训练期间保持不变</span></span><br><span class="line">        <span class="variable language_">self</span>.rand_weight = torch.rand((<span class="number">20</span>, <span class="number">20</span>), requires_grad=<span class="literal">False</span>)</span><br><span class="line">        <span class="variable language_">self</span>.linear = nn.Linear(<span class="number">20</span>, <span class="number">20</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        X = <span class="variable language_">self</span>.linear(X)</span><br><span class="line">        <span class="comment"># 使用创建的常量参数以及relu和mm函数(torch.mm是矩阵乘法)</span></span><br><span class="line">        X = F.relu(torch.mm(X, <span class="variable language_">self</span>.rand_weight) + <span class="number">1</span>)</span><br><span class="line">        <span class="comment"># 复用全连接层。这相当于两个全连接层共享参数</span></span><br><span class="line">        X = <span class="variable language_">self</span>.linear(X)</span><br><span class="line">        <span class="comment"># 控制流</span></span><br><span class="line">        <span class="keyword">while</span> X.<span class="built_in">abs</span>().<span class="built_in">sum</span>() &gt; <span class="number">1</span>:</span><br><span class="line">            X /= <span class="number">2</span></span><br><span class="line">        <span class="keyword">return</span> X.<span class="built_in">sum</span>()</span><br><span class="line">        </span><br><span class="line">net = FixedHiddenMLP()</span><br><span class="line">net(X)</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">随意组合嵌套层，块</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">NestMLP</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.net = nn.Sequential(nn.Linear(<span class="number">20</span>, <span class="number">64</span>), nn.ReLU(),</span><br><span class="line">                                 nn.Linear(<span class="number">64</span>, <span class="number">32</span>), nn.ReLU())</span><br><span class="line">        <span class="variable language_">self</span>.linear = nn.Linear(<span class="number">32</span>, <span class="number">16</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.linear(<span class="variable language_">self</span>.net(X))</span><br><span class="line"></span><br><span class="line">chimera = nn.Sequential(NestMLP(), nn.Linear(<span class="number">16</span>, <span class="number">20</span>), FixedHiddenMLP())</span><br><span class="line">chimera(X)</span><br></pre></td></tr></table></figure><h3 id="参数管理"><a href="#参数管理" class="headerlink" title="参数管理"></a>参数管理</h3><p>我们先定义一个MLP</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(nn.Linear(<span class="number">4</span>, <span class="number">8</span>), nn.ReLU(), nn.Linear(<span class="number">8</span>, <span class="number">1</span>))</span><br><span class="line">X = torch.rand(size=(<span class="number">2</span>, <span class="number">4</span>))</span><br><span class="line">net(X)</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">参数访问</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">块里面的层，也重载了[]方法，可以索引每一个组件。所以net[2]表示第三个组件：8x1的那个Linear</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Linear类有成员方法state_dict(), 用来返回这个层的参数</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].state_dict())</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">可以看到，就是全部打印出来了，包括这一层的权重矩阵W，以及偏置向量</span></span><br><span class="line"><span class="string">OrderedDict([(&#x27;weight&#x27;, tensor([[-0.0427, -0.2939, -0.1894,  0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), (&#x27;bias&#x27;, tensor([0.0887]))])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>你也可以指定Linear类的<strong>成员变量</strong>来访问具体的某一个参数</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="built_in">print</span>(<span class="built_in">type</span>(net[<span class="number">2</span>].bias))</span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].bias)</span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].bias.data)</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">&lt;class &#x27;torch.nn.parameter.Parameter&#x27;&gt;</span></span><br><span class="line"><span class="string">Parameter containing:</span></span><br><span class="line"><span class="string">tensor([0.0887], requires_grad=True)</span></span><br><span class="line"><span class="string">tensor([0.0887])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>每个参数对象，当然也有其梯度成员变量</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">net[<span class="number">2</span>].weight.grad == <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">True</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>嵌套网络也能直接print(net)来打出所有的块，层的参数</p><h3 id="参数初始化-1"><a href="#参数初始化-1" class="headerlink" title="参数初始化"></a>参数初始化</h3><p>默认情况下，pytorch会根据一个范围均匀的初始化W，b, 这个范围是根据输入输出的维度计算的。</p><p><strong>nn.init库提供多种初始化方法</strong></p><p><mark style="background:#affad1">nn.init库内置初始化</mark></p><ul><li><code>nn.init.normal_(参数张量对象，mean, std)</code> 正态分布</li><li><code>nn.init.zeros_(参数张量对象)</code> 0初始化</li><li><code>nn.init.constant_(参数张量对象，常数)</code> 常数初始化</li><li><code>nn.init.uniform_(参数张量对象, -10, 10)</code> 均匀分布</li><li><code>nn.init.xavier_uniform_(参数张量对象)</code> <ul><li>调用xavier初始化方法，他会自动根据输入输出维度来自动化初始化，保证方差，减少梯度爆炸</li></ul></li></ul><p>然后自定义自己的初始化工作流，用<code>nn.apply()</code>, 来调用你的初始化函数</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">init_normal</span>(<span class="params">m</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">        nn.init.normal_(m.weight, mean=<span class="number">0</span>, std=<span class="number">0.01</span>)</span><br><span class="line">        nn.init.zeros_(m.bias)</span><br><span class="line">net.apply(init_normal)</span><br><span class="line">net[<span class="number">0</span>].weight.data[<span class="number">0</span>], net[<span class="number">0</span>].bias.data[<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_xavier</span>(<span class="params">m</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">        nn.init.xavier_uniform_(m.weight)</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_42</span>(<span class="params">m</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">        nn.init.constant_(m.weight, <span class="number">42</span>)</span><br><span class="line"></span><br><span class="line">net[<span class="number">0</span>].apply(init_xavier)</span><br><span class="line">net[<span class="number">2</span>].apply(init_42)</span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">0</span>].weight.data[<span class="number">0</span>])</span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].weight.data)</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">参数绑定</mark><br>多个层间共享参数：我们可以定义一个稠密层，然后<strong>使用它的参数来设置另一个层的参数</strong>（说白了，就是底层是一个内存，共享内存）</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 我们需要给共享层一个名称，以便可以引用它的参数</span></span><br><span class="line">shared = nn.Linear(<span class="number">8</span>, <span class="number">8</span>)</span><br><span class="line">net = nn.Sequential(nn.Linear(<span class="number">4</span>, <span class="number">8</span>), nn.ReLU(),</span><br><span class="line">                    shared, nn.ReLU(),</span><br><span class="line">                    shared, nn.ReLU(),</span><br><span class="line">                    nn.Linear(<span class="number">8</span>, <span class="number">1</span>))</span><br><span class="line">net(X)</span><br><span class="line"><span class="comment"># 检查参数是否相同</span></span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].weight.data[<span class="number">0</span>] == net[<span class="number">4</span>].weight.data[<span class="number">0</span>])</span><br><span class="line">net[<span class="number">2</span>].weight.data[<span class="number">0</span>, <span class="number">0</span>] = <span class="number">100</span></span><br><span class="line"><span class="comment"># 确保它们实际上是同一个对象，而不只是有相同的值</span></span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">2</span>].weight.data[<span class="number">0</span>] == net[<span class="number">4</span>].weight.data[<span class="number">0</span>])</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">tensor([True, True, True, True, True, True, True, True])</span></span><br><span class="line"><span class="string">tensor([True, True, True, True, True, True, True, True])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><h3 id="延后初始化"><a href="#延后初始化" class="headerlink" title="延后初始化"></a>延后初始化</h3><p>我们之前都是显示定义<code>nn.Linear(20, 256)</code></p><p>pytorch现在也支持<strong>延后初始化</strong>，也就是<strong>懒惰模块</strong></p><p><img src="/images/Pasted%20image%2020260501163539.png" alt="586"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 LazyLinear，你不需要写输入维度</span></span><br><span class="line">net = nn.Sequential(</span><br><span class="line">    nn.LazyLinear(<span class="number">256</span>), </span><br><span class="line">    nn.ReLU(),</span><br><span class="line">    nn.LazyLinear(<span class="number">10</span>)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 此时打印参数，权重还是空的，因为 PyTorch 还没“见过”数据</span></span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">0</span>].weight) <span class="comment"># 会提示这是一个 UninitializedParameter</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 第一次“喂”数据时，延后初始化发生</span></span><br><span class="line">X = torch.rand(<span class="number">2</span>, <span class="number">20</span>) </span><br><span class="line">net(X) </span><br><span class="line"></span><br><span class="line"><span class="comment"># 现在 PyTorch 已经推断出输入是 20，并完成了初始化</span></span><br><span class="line"><span class="built_in">print</span>(net[<span class="number">0</span>].weight.shape) <span class="comment"># 输出: torch.Size([256, 20])</span></span><br></pre></td></tr></table></figure><h3 id="自定义层"><a href="#自定义层" class="headerlink" title="自定义层"></a>自定义层</h3><p>这个没什么好说的，就是自己继承nn.Module基类，自己手搓组件</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyLinear</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, in_units, units</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.weight = nn.Parameter(torch.randn(in_units, units))</span><br><span class="line">        <span class="variable language_">self</span>.bias = nn.Parameter(torch.randn(units,))</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        linear = torch.matmul(X, <span class="variable language_">self</span>.weight.data) + <span class="variable language_">self</span>.bias.data</span><br><span class="line">        <span class="keyword">return</span> F.relu(linear)</span><br></pre></td></tr></table></figure><h3 id="torch读写文件"><a href="#torch读写文件" class="headerlink" title="torch读写文件"></a>torch读写文件</h3><p>训练过程中，肯定需要读写到磁盘里面</p><p><mark style="background:#affad1">加载和保存张量, 模型参数</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> torch.nn <span class="keyword">as</span> nn</span><br><span class="line"><span class="keyword">import</span> torch.nn.functional <span class="keyword">as</span> F</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;保存，加载张量&quot;&quot;&quot;</span></span><br><span class="line">x = torch.load(<span class="string">&#x27;x-file&#x27;</span>) <span class="comment"># 从x-file文件中，加载张量</span></span><br><span class="line">y = torch.zeros(<span class="number">4</span>)</span><br><span class="line">torch.save([x,y], <span class="string">&#x27;x-files&#x27;</span>)</span><br><span class="line">x2,y2 = torch.load(<span class="string">&#x27;x-files&#x27;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MLP</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.hidden = nn.Linear(<span class="number">20</span>, <span class="number">256</span>)</span><br><span class="line">        <span class="variable language_">self</span>.output = nn.Linear(<span class="number">256</span>, <span class="number">10</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.output(F.relu(<span class="variable language_">self</span>.hidden(x)))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 模型会自动初始化</span></span><br><span class="line">net = MLP()</span><br><span class="line">X = torch.randn(size=(<span class="number">2</span>, <span class="number">20</span>))</span><br><span class="line">Y = net(X)</span><br><span class="line">        </span><br><span class="line"><span class="string">&quot;&quot;&quot;保存，加载模型参数&quot;&quot;&quot;</span></span><br><span class="line">torch.save(net.state_dict(), <span class="string">&#x27;mlp.params&#x27;</span>)</span><br><span class="line"></span><br><span class="line">clone = MLP() <span class="comment"># 此时模型参数为空</span></span><br><span class="line">clone.load_state_dict(torch.load(<span class="string">&#x27;mlp.params&#x27;</span>))</span><br><span class="line">clone.<span class="built_in">eval</span>() </span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">用来把模型切换到**评估模式（Evaluation Mode）**</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>调用 <code>model.eval()</code> 后，模型会自动关闭所有<strong>只在训练阶段生效、影响梯度 &#x2F; 输出的层</strong>，比如：</p><ul><li><strong>Dropout 层</strong>：停止随机丢弃神经元，所有神经元都参与计算</li><li><strong>BatchNorm 层</strong>：停止用当前 batch 的均值 &#x2F; 方差做归一化，改用训练阶段保存好的全局均值 &#x2F; 方差</li><li>其他训练专属的层（如 LayerNorm 相关的训练逻辑）也会进入评估状态</li></ul><h2 id="CNN-卷积"><a href="#CNN-卷积" class="headerlink" title="CNN 卷积"></a>CNN 卷积</h2><p>前面的FNN，输入是一个轴，现在变成两个轴，最典型的就是图像数据了，不过当时我们只是把图像用nn.Flatten()层展平成一个轴。忽略了每个图像的空间结构信息。</p><p>这里开始介绍卷积神经网络CNN，这是<mark style="background:#fff88f">专门为处理图像数据而设计的神经网络</mark>。</p><p><mark style="background:#affad1">CV中的要求</mark></p><ul><li><strong><em>平移不变性</em></strong>（translation invariance）：不管检测对象出现在图像中的哪个位置，神经网络的前面几层应该对相同的图像区域具有相似的反应，即为“平移不变性”<ul><li>具体说就是，输入X的偏移，仅导致该层输出的偏移，而该层的模型参数不变。</li></ul></li><li><strong><em>局部性</em></strong>（locality）：神经网络的前面几层应该<strong>只探索输入图像中的局部区域</strong>，而不过度在意图像中相隔较远区域的关系，这就是“局部性”原则。最终，<strong>可以聚合这些局部特征</strong>，以在整个图像级别进行预测</li></ul><h3 id="卷积层"><a href="#卷积层" class="headerlink" title="卷积层"></a>卷积层</h3><p><mark style="background:#affad1">卷积的推导</mark><br>可以看到，就是从MLP的一维拓展到二维，然后根据平移不变性得到的卷积公式<br><img src="/images/3c72b446b5a0437ac8b2e65ad706e5f1.jpg" alt="520"><br>之后再根据局部性改写一下<strong>卷积层</strong>的函数<br><img src="/images/96a29d992496b2b6c369ebe505222ff0.jpg" alt="604"></p><p>里面的<strong>V矩阵</strong>就被称为<strong>卷积核</strong>&#x2F;<strong>滤波器</strong>&#x2F;<strong>该卷积层的权重矩阵</strong>（通常，<mark style="background:#fff88f">该权重就是可学习的参数</mark>）<br>Δ就表示滤波器的尺寸</p><p>当我们处理的局部区域Δ很小的时候，</p><ul><li>卷积神经网络比MLP需要<strong>更少的网络参数</strong>，</li><li>但是<strong>代价是我们的特征是平移不变</strong></li><li>而且并且当确定每个<strong>隐藏活性值</strong>时，每一层<strong>只包含局部的信息</strong><br><img src="/images/72ec2de72a5845460fdc32c221070474.jpg" alt="470"></li></ul><p>所以，卷积层，全连接层，都只是，不同功能的一个层而已，本质上都是输入到输出（预激活值&#x2F;隐藏活性值）的一个映射关系。</p><p><mark style="background:#fff88f">全连接层：权重矩阵W，偏置向量b, 是需要学习的参数</mark><br><mark style="background:#fff88f">卷积层：卷积核矩阵（Δ，Δ），是需要学习的参数</mark></p><p>通过训练，来让我们的模型拟合训练数据（说白了，就是为了训练有效的卷积核）</p><p>下面再看一下数学上，卷积的公式定义<br><img src="/images/Pasted%20image%2020260502102739.png"></p><p>下面再解释一下<mark style="background:#affad1">通道</mark><br>一般图像，可能一个像素，不是一个标量，而是一个3维的张量，因为有R，G，B三个通道。</p><p>所以一张图片样本的形状是（通道x 高度 x 宽度）</p><p>因此我们将X输入图片，变成3维，然后卷积核也增加成3维<br><img src="/images/Pasted%20image%2020260502103724.png" alt="374"><br><img src="/images/ca348a267ad9a02ae0fbd72ea9366d12.jpg" alt="&lt;font color&#x3D;&quot;#ff0000&quot;&gt;这里其实还差最后一步，一个卷积核的各个通道的输出结果，要加起来，变成单通道的输出&lt;&#x2F;font&gt;|488"></p><p>下面看一下卷积层，具体是如何计算的，这里我们的卷积运算，所表达的其实是，<strong>互相关运算</strong><br>在卷积层中，<strong>输入张量</strong>和<strong>核张量</strong>通过<mark style="background:#fff88f">互相关运算</mark>产生<strong>输出张量</strong></p><p><img src="/images/Pasted%20image%2020260502104035.png"></p><p>所以，知道了互相关运算的公式，你就可以自己实现卷积核运算了</p><p>知道了卷积核的一次互相关运算，我们来看一下<mark style="background:#affad1">卷积层，是如何完成这一层的计算的。</mark></p><p><strong>卷积层对输入和卷积核权重进行互相关运算，并在添加标量偏置之后产生输出</strong></p><p>所以卷积层中的两个被训练的参数：</p><ul><li><mark style="background:#fff88f">卷积核权重矩阵</mark></li><li><mark style="background:#fff88f">标量偏置</mark><br>所以我们在模型一开始，肯定也要初始化卷积层的这些参数</li></ul><p>下面我们自定义一个自己的2维卷积层</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Conv2D</span>(nn.Module):</span><br><span class="line"><span class="comment"># kernel_size是一个张量（3，4），注意，卷积核不一定是正方形的</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, kernel_size</span>): </span><br><span class="line"><span class="built_in">super</span>().__init__()</span><br><span class="line"><span class="variable language_">self</span>.weight = nn.Parameter(torch.rand(kernel_size))</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">nn.Parameter()，是为了把变量注册成模型参数，这样你可以通过model.parameters()来获取。同时，优化器可以定位到这些权重。</span></span><br><span class="line"><span class="string">同时自动把requires_grad 设置为 True。</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="variable language_">self</span>.bias = nn.Parameter(torch.zeros(<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x</span>):</span><br><span class="line"><span class="keyword">return</span> corr2d(x, <span class="variable language_">self</span>.weight)+<span class="variable language_">self</span>.bias</span><br></pre></td></tr></table></figure><p>高h, 宽w的卷积核，被称为 hxw卷积核。该卷积层，被称为hxw卷积层</p><h3 id="卷积核举例"><a href="#卷积核举例" class="headerlink" title="卷积核举例"></a>卷积核举例</h3><p>比如对于一个0，1的图像<br><img src="/images/Pasted%20image%2020260502105645.png" alt="331"></p><p>我们想检测他的边界线，也就是0，1的边界</p><p>我们就构造一个（1，2）的卷积核<code>K = torch.tensor([[1.0, -1.0]])</code></p><p>对该图像卷积之后，</p><p><img src="/images/Pasted%20image%2020260502105801.png" alt="337"></p><p>但这个时候，如果现在我们<mark style="background:#ff4d4f">将输入的二维图像转置</mark>，再进行如上的互相关运算。 其输出如下，之前检测到的垂直边缘消失了。 不出所料，这个卷积核<code>K</code>只可以检测垂直边缘，无法检测水平边缘。<mark style="background:#ff4d4f">卷积核失效了</mark></p><p>所以一个特定的卷积核，只能提取一种特征。</p><p>所以我们的目的，就是要找到适合这个特征的卷积核，也就是要去学习卷积核，对应于MLP的拟合参数。</p><ul><li>MLP叫拟合参数（优化参数）</li><li>卷积叫学习卷积核</li></ul><h3 id="学习卷积核（优化参数）"><a href="#学习卷积核（优化参数）" class="headerlink" title="学习卷积核（优化参数）"></a>学习卷积核（优化参数）</h3><p>当有了更复杂数值的卷积核，或者连续的卷积层时，我们不可能手动设计滤波器。那么我们是否可以学习由<code>X</code>生成<code>Y</code>的卷积核呢</p><p><strong>仅查看“输入-输出”对来学习由<code>X</code>生成<code>Y</code>的卷积核</strong></p><p>这里还是用的标签来实现的，然后计算该样本的平方差损失</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 构造一个二维卷积层，它具有1个输出通道和形状为（1，2）的卷积核</span></span><br><span class="line">conv2d = nn.Conv2d(<span class="number">1</span>,<span class="number">1</span>, kernel_size=(<span class="number">1</span>, <span class="number">2</span>), bias=<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 这个二维卷积层使用四维输入和输出格式（批量大小、通道、高度、宽度），</span></span><br><span class="line"><span class="comment"># 其中批量大小和通道数都为1</span></span><br><span class="line">X = X.reshape((<span class="number">1</span>, <span class="number">1</span>, <span class="number">6</span>, <span class="number">8</span>))</span><br><span class="line">Y = Y.reshape((<span class="number">1</span>, <span class="number">1</span>, <span class="number">6</span>, <span class="number">7</span>))</span><br><span class="line">lr = <span class="number">3e-2</span>  <span class="comment"># 学习率</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">10</span>):</span><br><span class="line">    Y_hat = conv2d(X)</span><br><span class="line">    l = (Y_hat - Y) ** <span class="number">2</span></span><br><span class="line">    conv2d.zero_grad()</span><br><span class="line">    l.<span class="built_in">sum</span>().backward()</span><br><span class="line">    <span class="comment"># 迭代卷积核</span></span><br><span class="line">    conv2d.weight.data[:] -= lr * conv2d.weight.grad</span><br><span class="line">    <span class="keyword">if</span> (i + <span class="number">1</span>) % <span class="number">2</span> == <span class="number">0</span>:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&#x27;epoch <span class="subst">&#123;i+<span class="number">1</span>&#125;</span>, loss <span class="subst">&#123;l.<span class="built_in">sum</span>():<span class="number">.3</span>f&#125;</span>&#x27;</span>)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260502112604.png" alt="522"><br>发现还是可以学会的，非常接近原来我们设计的[1,-1]的卷积核</p><p>为了与深度学习文献中的标准术语保持一致，我们<strong>将继续把“互相关运算”称为卷积运算</strong>，尽管严格地说，它们略有不同。 此外，对于<strong>卷积核张量上的权重，我们称其为_元素_</strong></p><p><mark style="background:#affad1">特征映射和感受野</mark><br>卷积层，有时候，也被称为特征映射，因为被视为从一个输入，映射到下一层的空间维度的转换器。</p><p><strong>感受野</strong>，是指的所有贡献该层的元素的前向传播的所有元素。</p><h3 id="卷积细节"><a href="#卷积细节" class="headerlink" title="卷积细节"></a>卷积细节</h3><h4 id="填充与步幅"><a href="#填充与步幅" class="headerlink" title="填充与步幅"></a>填充与步幅</h4><p>前面我们 3x3 的输入，经过 2x2的卷积，最终得到了输出2x2， 发现尺寸变小了<br><img src="/images/Pasted%20image%2020260502113211.png" alt="249"></p><p><img src="/images/Pasted%20image%2020260502113303.png"></p><p><mark style="background:#affad1">填充</mark><br>可以发现，在应用多层卷积的时候，我们就逐渐丢失边缘像素了，多层之后，丢失的越来越多。</p><p>因此，保证每次卷积之后，<strong>输入输出尺寸不变的办法</strong>，就是<strong>填充</strong></p><p><img src="/images/Pasted%20image%2020260502113534.png"></p><p><strong>卷积神经网络中卷积核的高度和宽度通常为奇数，例如1、3、5或7</strong><br>选择<strong>奇数的好处</strong>是，</p><ul><li>保持空间维度的同时，我们可以在顶部和底部填充相同数量的行，在左侧和右侧填充相同数量的列</li><li>使用奇数的核大小和填充大小也提供了书写上的便利：<ul><li>对于任何二维张量<code>X</code>，当满足： 1. <strong>卷积核</strong>的大小是<strong>奇数</strong>； 2. 所有边的<strong>填充</strong>行数和列数<strong>相同</strong>； 3. <strong>输出与输入具有相同高度和宽度</strong> 则可以得出：输出<code>Y[i, j]</code>是通过以输入<code>X[i, j]</code>为中心，与卷积核进行互相关计算得到的。</li></ul></li></ul><p>下面我们来写一下这个填充，然后让卷积层保证输入输出同尺寸</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">nn.Conv2d(1,1)</span></span><br><span class="line"><span class="string">第一个1， in_channels, 表示输入样本的通道，我们是单通道</span></span><br><span class="line"><span class="string">第二个1， out_channels, 卷积后输出的通道数，表明会有多少个卷积核</span></span><br><span class="line"><span class="string">kernel_size = 3, 为python的语法糖，效果等同于kernel_size=(3,3),你也可以传入（5，3）</span></span><br><span class="line"><span class="string">padding=1，表示在输入的二维图像的，上下左右，各填充一行0（默认是0）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">conv2d = nn.Conv2d(<span class="number">1</span>, <span class="number">1</span>, kernel_size=<span class="number">3</span>， padding =<span class="number">1</span>)</span><br><span class="line">X= torch.rand(<span class="number">8</span>,<span class="number">8</span>)</span><br></pre></td></tr></table></figure><p>这样,我们指定了输入图像为8x8的尺寸，单通道 -&gt; in_channels &#x3D; 1<br>然后用nn.Conv2d来创建了一个卷积层对象</p><p>这里需要解释的是：</p><ul><li><code>in_channels</code> 决定了每个卷积核长什么样（单个卷积核本身有多少个通道）<ul><li>单个卷积核的通道数必须等于输入图片的通道数</li><li><img src="/images/Pasted%20image%2020260502145205.png"></li></ul></li><li>而 <code>out_channels</code> 决定了你有多少个卷积核。<ul><li><img src="/images/Pasted%20image%2020260502145252.png"></li></ul></li></ul><p><img src="/images/Pasted%20image%2020260502145351.png" alt="434"></p><p><img src="/images/Pasted%20image%2020260502145407.png" alt="583"></p><p>所以，一个卷积层里面，会有多个卷积核，类比于一个隐藏层的隐藏单元，线性回归单元</p><p><img src="/images/264d2bd4313074705a9b2519b747a98e.jpg"><br><img src="/images/Pasted%20image%2020260502152534.png" alt="这里就展示了2通道的卷积核，最终输出1通道的输出|315"></p><p>定义好了卷积层之后</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line">conv2d = nn.Conv2d(<span class="number">1</span>, <span class="number">1</span>, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">创建出的卷积核shape(1,1,3,3), 使用kaiming均匀分布初始化权重还有偏置标量</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">X = torch.rand(size=(<span class="number">8</span>, <span class="number">8</span>)).reshape((<span class="number">1</span>,<span class="number">1</span>)+X.shape) <span class="comment"># (1,1)+(8,8)是元组拼接</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">conv2d()卷积层，他的</span></span><br><span class="line"><span class="string">输入张量的格式(batch_size, in_channels输入图片的通道数=卷积核通道, height, width)</span></span><br><span class="line"><span class="string">输出张量的格式(batch_size, out_channels输出特征的通道数=卷积核数量, height, width)</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">comp_conv2d</span>(<span class="params">conv2d, X</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">卷积层conv2d的输入输出格式（批量大小，通道，）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">Y = conv2d(X) </span><br><span class="line"><span class="keyword">return</span> Y.reshape(Y.shape[<span class="number">2</span>:]) <span class="comment"># 打印出shape数组的2，3位，就是(weight, width)</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">最终返回torch.Size([8, 8])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>以上，我们就已经了解了一个卷积层的内部，最后总结一下<br><img src="/images/Pasted%20image%2020260502151854.png" alt="334"></p><h4 id="步幅"><a href="#步幅" class="headerlink" title="步幅"></a>步幅</h4><p>这个就是指的一个卷积核，如何在输入图片上移动的步长了，你可以指定每次滑动的步幅<br><img src="/images/Pasted%20image%2020260502152159.png" alt="434"><br>可以看到，当stride改成2的时候，输出尺寸从8x8, 变成了4x4了</p><p>同样复杂一点指定<strong>非正方形的卷积</strong><br><img src="/images/Pasted%20image%2020260502152255.png"></p><p><mark style="background:#fff88f">在实践中，我们很少使用不一致的步幅或填充</mark></p><p>如何根据<mark style="background:#fff88f">输入的尺寸</mark>，<mark style="background:#fff88f">卷积核的尺寸</mark>，以及<mark style="background:#fff88f">padding</mark>， <mark style="background:#fff88f">stride</mark>来计算输出图片的尺寸，由公式<br><img src="/images/Pasted%20image%2020260502151940.png"></p><h3 id="1x1卷积层"><a href="#1x1卷积层" class="headerlink" title="1x1卷积层"></a>1x1卷积层</h3><p>当我们的卷积核的高度&#x3D;宽度&#x3D;1时，每个卷积核通道内部的计算消失了，<strong>只剩下了通道上的计算</strong>。</p><p>$1 \times 1$ 卷积的唯一计算发生在<strong>通道</strong>上。</p><p>输出中的每个元素都是从输入图像中<strong>同一位置</strong>但在不同通道上的元素的<strong>线性组合</strong>。（<font color="#00b050">相当于像素级别的全连接，只不过不同像素的权重都是一样的</font>）</p><p>这使得网络能够有效地<strong>整合跨通道的特征信息</strong>。</p><p><strong>他的最大的用处</strong>是：<mark style="background:#fff88f">灵活调整通道数（升维或降维）</mark></p><ul><li>通过设置不同的输出通道数（$c_o$），$1 \times 1$ 卷积可以用来<strong>增加或减少通道的维度</strong>。</li><li>这常用于在不改变图像高度和宽度（空间分辨率）的前提下，<strong>压缩模型参数</strong>或<strong>增加特征的丰富度</strong><br><img src="/images/a3bd21f3a7fb8848ed8b3c8b9a569235.jpg" alt="570"></li></ul><h3 id="汇聚层（池化层）"><a href="#汇聚层（池化层）" class="headerlink" title="汇聚层（池化层）"></a>汇聚层（池化层）</h3><p>当我们处理图像的时候，中间的卷积层，会产生多通道的同尺寸图片，每个通道表示一个卷积核通道卷出来的特征。</p><p>如果我们希望降低这些中间图片特征所表示的空间分辨率，聚集信息。这样我们每个神经元对其敏感的感受野就越大了（相当于压缩了嘛）</p><p>因为我们的机器学习的任务，是和全局图像有关的，所以我们最后一层的神经元，应该对整个输入的全局是敏感的。所以要通过池化操作，来聚合信息，生成越来越粗糙的映射，最终学会全局表示的目标，同时将卷积图层的所有又是保留在中间层。</p><p>所以:<br><em>汇聚</em>（pooling）层，它具有双重目的：</p><ul><li>降低卷积层对位置的敏感性(肯定不希望稍微偏移一个像素就识别不出来)</li><li>同时降低对空间降采样表示的敏感性</li></ul><p>所以我们的<strong>池化操作，主要是针对空间信息</strong>， 前面的<strong>1x1卷积，主要是针对通道特征信息</strong></p><h5 id="最大汇聚层-平均汇聚层"><a href="#最大汇聚层-平均汇聚层" class="headerlink" title="最大汇聚层&#x2F;平均汇聚层"></a>最大汇聚层&#x2F;平均汇聚层</h5><p><img src="/images/Pasted%20image%2020260502155141.png"><br>所以，本质上，池化窗口，也是一个区域，类似卷积核，只不过他不是互相关计算，而是<strong>计算该窗口的输入子张量的最大值&#x2F;平均值</strong>， 分别对应了<strong>最大汇聚层</strong>，<strong>平均汇聚层</strong></p><p><strong>汇聚窗口形状位pxq的汇聚层，称为pxq汇聚层</strong></p><blockquote><p>这样，使用2x2的汇聚层，即使在高度，宽度上移动一个像素，卷积层仍可以识别到模式</p></blockquote><h4 id="填充和步幅"><a href="#填充和步幅" class="headerlink" title="填充和步幅"></a>填充和步幅</h4><p>因为池化层也是一个窗口计算，所以肯定要移动，这就又带来了填充问题和步幅问题。从计算上来看，是一样的。</p><p>但是移动方式上，和卷积核不太一样，因为我们是池化操作，所以是<strong>降采样，因此一次移动的步幅，默认等于池化窗口的尺寸，你也可以自己指定步幅</strong><br><img src="/images/Pasted%20image%2020260502160254.png" alt="266"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">X = torch.arange(<span class="number">16</span>, dtype=torch.float32).reshape((<span class="number">1</span>, <span class="number">1</span>, <span class="number">4</span>, <span class="number">4</span>))</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">tensor([[[[ 0.,  1.,  2.,  3.],</span></span><br><span class="line"><span class="string">          [ 4.,  5.,  6.,  7.],</span></span><br><span class="line"><span class="string">          [ 8.,  9., 10., 11.],</span></span><br><span class="line"><span class="string">          [12., 13., 14., 15.]]]])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">pool2d = nn.MaxPool2D(<span class="number">3</span>)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">我们自己定义一个最大池化层，形状（3，3），因此步幅也为（3，3）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">poll2d(X)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">tensor([[[[10.]]]]), 因为一步也移动不了</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">pool2d = nn.MaxPool2d(<span class="number">3</span>, padding=<span class="number">1</span>, stride=<span class="number">2</span>)</span><br><span class="line">pool2d(X)</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">tensor([[[[ 5.,  7.],</span></span><br><span class="line"><span class="string">          [13., 15.]]]])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><h3 id="经典CNN（LeNet）"><a href="#经典CNN（LeNet）" class="headerlink" title="经典CNN（LeNet）"></a>经典CNN（LeNet）</h3><p>总体来看，LeNet（LeNet-5）由两个部分组成：</p><ul><li>卷积编码器：由两个卷积层组成;</li><li>全连接层密集块：由三个全连接层组成。<br><img src="/images/Pasted%20image%2020260502160405.png"></li></ul><p><strong>每个卷积块</strong>中的基本单元是<strong>一个卷积层</strong>、一个sigmoid<strong>激活函数</strong>和<strong>平均汇聚层</strong><br>(ReLU和最大汇聚层更有效，但是当时还没出现)</p><p>这边提一下CNN的卷积核的参数的训练（因为他这个参数量很少）<br><img src="/images/913c6f7b78a1e9becbbce7005f300526.jpg" alt="452"></p><p><mark style="background:#fff88f">以 LeNet 处理 MNIST 手写数字为例</mark>：</p><ol><li><strong>前向传播</strong>：输入图像经过 $5 \times 5$ 卷积，得到特征图。</li><li><strong>计算误差</strong>：在输出层计算预测值与真实标签（如数字 “7”）的损失 $L$。</li><li><strong>误差回传</strong>：<ul><li>误差从全连接层传回池化层。</li><li>从池化层传回卷积层（得到 $\frac{\partial L}{\partial Y}$）。</li></ul></li><li><strong>权重更新</strong>：<ul><li>利用上述的“累加梯度”公式算出卷积核的梯度 $\frac{\partial L}{\partial W}$。</li><li><strong>更新参数</strong>：$W_{new} &#x3D; W_{old} - \eta \cdot \frac{\partial L}{\partial W}$（$\eta$ 是学习率）。</li></ul></li></ol><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(</span><br><span class="line">    nn.Conv2d(<span class="number">1</span>, <span class="number">6</span>, kernel_size=<span class="number">5</span>, padding=<span class="number">2</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>),</span><br><span class="line">    nn.Conv2d(<span class="number">6</span>, <span class="number">16</span>, kernel_size=<span class="number">5</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>),</span><br><span class="line">    nn.Flatten(),</span><br><span class="line">    nn.Linear(<span class="number">16</span> * <span class="number">5</span> * <span class="number">5</span>, <span class="number">120</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">120</span>, <span class="number">84</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">84</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260502161900.png" alt="156"></p><p><code>with torch.no_grad():</code><br>这个的作用，是关闭保留中间变量的操作，因此可以：</p><ul><li>加快测试速度</li><li>减小内存占用</li><li>避免改变模型权重</li></ul><p><strong>自动梯度是前向 “留后路（建图存中间）” 给反向用；</strong><br><strong>推理不需要后路，no_grad 就是把这条后路直接封死，不占空间、不浪费性能。</strong></p><h3 id="现代卷积神经网络"><a href="#现代卷积神经网络" class="headerlink" title="现代卷积神经网络"></a>现代卷积神经网络</h3><p>在LeNet出来之后，开始了现代卷积神经网络</p><p>LeNet在小数据集上效果还行，但是更大，更真实的数据集上，效果就拉了。</p><p>在此之前，人们倾向于自己提取自己理解的特征，比如HOG，SIFT这些。<br>但是Alex这些人，他们认为：<strong>特征本身应该被学习</strong></p><p><strong>在合理地复杂性前提下，特征应该由多个共同学习的神经网络层组成，每个层都有可学习的参数</strong></p><p>在机器视觉中，最底层可能检测边缘、颜色和纹理，由此诞生_<strong>AlexNet</strong>_</p><ul><li>在<strong>网络的最底层</strong>，模型学习到了一些类似于<strong>传统滤波器的特征抽取器</strong><ul><li><img src="/images/Pasted%20image%2020260502163030.png" alt="300"></li></ul></li><li>AlexNet的<strong>更高层建立在这些底层表示的基础上</strong>，以表示更大的特征，如<strong>眼睛</strong>、<strong>鼻子</strong>、草叶等等</li><li><strong>更高的层</strong>可以检测<strong>整个物体</strong>，如人、飞机、狗或飞盘</li><li><strong>最终的隐藏神经元</strong>可以学习图像的综合表示</li></ul><h4 id="GPU并行计算优势"><a href="#GPU并行计算优势" class="headerlink" title="GPU并行计算优势"></a>GPU并行计算优势</h4><p><img src="/images/Pasted%20image%2020260502164201.png"></p><p><mark style="background:#fff88f">深度学习里面，还有哪些环节能在GPU上并行呢？</mark></p><ul><li><strong>卷积层运算</strong>：<img src="/images/Pasted%20image%2020260502164336.png" alt="505"></li><li><strong>激活函数</strong>：<img src="/images/Pasted%20image%2020260502164419.png" alt="519"></li><li><strong>池化层</strong>：这个和卷积类似</li><li><strong>全连接层</strong>：本质还是矩阵乘法</li><li><strong>损失函数计算</strong>：<img src="/images/Pasted%20image%2020260502164526.png" alt="463"></li><li><strong>反向传播</strong>：<img src="/images/Pasted%20image%2020260502164604.png" alt="585"></li></ul><p>所有能并行的环节，都满足一个共同点：<strong>计算任务之间没有数据依赖</strong></p><h4 id="ai-infra推理加速"><a href="#ai-infra推理加速" class="headerlink" title="ai infra推理加速"></a>ai infra推理加速</h4><p>AI Infra 在模型推理加速上，核心就是<strong>想尽办法让 GPU&#x2F;CPU 少做无用功、多做有效并行、减少数据瓶颈</strong>，把模型跑的又快又省</p><p><img src="/images/Pasted%20image%2020260502164901.png" alt="565"></p><p><img src="/images/Pasted%20image%2020260502164912.png"></p><p><img src="/images/Pasted%20image%2020260502164918.png"></p><p><img src="/images/Pasted%20image%2020260502164926.png"></p><h5 id="多GPU训练"><a href="#多GPU训练" class="headerlink" title="多GPU训练"></a>多GPU训练</h5><p>一种<strong>分模型参数</strong>，一种<strong>不分参数只分数据</strong><br><img src="/images/Pasted%20image%2020260502165907.png" alt="419"></p><p><img src="/images/Pasted%20image%2020260502170000.png" alt="318"><img src="/images/Pasted%20image%2020260502170017.png" alt="335"></p><p><img src="/images/Pasted%20image%2020260502170129.png"></p><h4 id="AlexNet"><a href="#AlexNet" class="headerlink" title="AlexNet"></a>AlexNet</h4><p>AlexNet横空出世。它首次证明了<strong>学习到的特征</strong>可以<strong>超越手工设计的特征</strong></p><p> AlexNet使用了<strong>8层卷积神经网络</strong></p><p><img src="/images/Pasted%20image%2020260502165049.png" alt="364"></p><ol><li>AlexNet比相对较小的LeNet5要深得多。AlexNet由八层组成：五个卷积层、两个全连接隐藏层和一个全连接输出层。</li><li>AlexNet使用ReLU而不是sigmoid作为其激活函数。</li></ol><p>下面来分析一下AlexNet的架构设计<br>第一层卷积：11x11的卷积核，因为输入图片尺寸大，所以用大卷积核。<br>第二层卷积：5x5的卷积核<br>第三层卷积：3x3卷积<br>……<br>最后两个4096个输出的全连接层，总参数将近1GB</p><p><strong>激活函数选择ReLU</strong>, 因为当sigmoid激活函数的输出非常接近于0或1时，这些区域的梯度几乎为0，因此反向传播无法继续更新一些模型参数。 相反，ReLU激活函数在正区间的梯度总是1。 因此，如果模型参数没有正确初始化，sigmoid函数可能在正区间内得到几乎为0的梯度，从而使模型无法得到有效的训练</p><p>同时<strong>AlexNet通过暂退法</strong>控制全连接的<strong>模型复杂度</strong>。而LeNet只使用了权重衰减</p><p>为了进一步扩充数据，AlexNet在训练时增加了大量的图像<strong>增强数据</strong>，如翻转、裁切和变色</p><p>这使得模型更健壮，更大的样本量有效地<strong>减少了过拟合</strong></p><p>手动实现一个AlexNet</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(</span><br><span class="line">    <span class="comment"># 这里使用一个11*11的更大窗口来捕捉对象。</span></span><br><span class="line">    <span class="comment"># 同时，步幅为4，以减少输出的高度和宽度。</span></span><br><span class="line">    <span class="comment"># 另外，输出通道的数目远大于LeNet</span></span><br><span class="line">    nn.Conv2d(<span class="number">1</span>, <span class="number">96</span>, kernel_size=<span class="number">11</span>, stride=<span class="number">4</span>, padding=<span class="number">1</span>), nn.ReLU(),</span><br><span class="line">    nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>),</span><br><span class="line">    <span class="comment"># 减小卷积窗口，使用填充为2来使得输入与输出的高和宽一致，且增大输出通道数</span></span><br><span class="line">    nn.Conv2d(<span class="number">96</span>, <span class="number">256</span>, kernel_size=<span class="number">5</span>, padding=<span class="number">2</span>), nn.ReLU(),</span><br><span class="line">    nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>),</span><br><span class="line">    <span class="comment"># 使用三个连续的卷积层和较小的卷积窗口。</span></span><br><span class="line">    <span class="comment"># 除了最后的卷积层，输出通道的数量进一步增加。</span></span><br><span class="line">    <span class="comment"># 在前两个卷积层之后，汇聚层不用于减少输入的高度和宽度</span></span><br><span class="line">    nn.Conv2d(<span class="number">256</span>, <span class="number">384</span>, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>), nn.ReLU(),</span><br><span class="line">    nn.Conv2d(<span class="number">384</span>, <span class="number">384</span>, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>), nn.ReLU(),</span><br><span class="line">    nn.Conv2d(<span class="number">384</span>, <span class="number">256</span>, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>), nn.ReLU(),</span><br><span class="line">    nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>),</span><br><span class="line">    nn.Flatten(),</span><br><span class="line">    <span class="comment"># 这里，全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合</span></span><br><span class="line">    nn.Linear(<span class="number">6400</span>, <span class="number">4096</span>), nn.ReLU(),</span><br><span class="line">    nn.Dropout(p=<span class="number">0.5</span>),</span><br><span class="line">    nn.Linear(<span class="number">4096</span>, <span class="number">4096</span>), nn.ReLU(),</span><br><span class="line">    nn.Dropout(p=<span class="number">0.5</span>),</span><br><span class="line">    <span class="comment"># 最后是输出层。由于这里使用Fashion-MNIST，所以用类别数为10，而非论文中的1000</span></span><br><span class="line">    nn.Linear(<span class="number">4096</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><h4 id="VGG网络（块"><a href="#VGG网络（块" class="headerlink" title="VGG网络（块)"></a>VGG网络（块)</h4><p>虽然<strong>AlexNet</strong>证明<strong>深层神经网络卓有成效</strong>，但它<strong>没有提供一个通用的模板</strong>来指导后续的研究人员<strong>设计新的网络</strong></p><p>VGG网络中使用块的概念，方便了设计新网络的能力</p><p><mark style="background:#affad1">经典卷积神经网络的基本组成部分</mark>是下面的这个序列：</p><ol><li>带填充以保持分辨率的卷积层；</li><li>非线性激活函数，如ReLU；</li><li>汇聚层，如最大汇聚层。</li></ol><p><mark style="background:#affad1">VGG网络可以分为两部分</mark>：</p><ul><li>第一部分主要由<strong>卷积层</strong>和<strong>汇聚层</strong>组成</li><li>第二部分由<strong>全连接层</strong>组成</li></ul><p>具体给架构设计是这样子的</p><p><img src="/images/Pasted%20image%2020260502202615.png" alt="316"></p><p>这里我们自己实现一个vgg块</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> mxnet <span class="keyword">import</span> np, npx</span><br><span class="line"><span class="keyword">from</span> mxnet.gluon <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> mxnet <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line">npx.set_np()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">vgg_block</span>(<span class="params">num_convs, num_channels</span>):</span><br><span class="line">    blk = nn.Sequential()</span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(num_convs):</span><br><span class="line">        blk.add(nn.Conv2D(num_channels, kernel_size=<span class="number">3</span>,</span><br><span class="line">                          padding=<span class="number">1</span>, activation=<span class="string">&#x27;relu&#x27;</span>))</span><br><span class="line">    blk.add(nn.MaxPool2D(pool_size=<span class="number">2</span>, strides=<span class="number">2</span>))</span><br><span class="line">    <span class="keyword">return</span> blk</span><br></pre></td></tr></table></figure><p>VGG神经网络连接 <a href="https://zh-v2.d2l.ai/chapter_convolutional-modern/vgg.html#fig-vgg">图7.2.1</a>的几个VGG块（在<code>vgg_block</code>函数中定义）</p><p>其中有超参数变量<code>conv_arch</code>。该变量指定了每个<strong>VGG块</strong>里<strong>卷积层个数</strong>和<strong>输出通道数</strong>。<br><code>conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))</code> <strong>五个卷积块</strong></p><p><strong>全连接模块</strong>则与AlexNet中的相同。</p><p><strong>原始VGG网络有5个卷积块</strong>，其中<strong>前两个块</strong>各有一个卷积层，<strong>后三个块</strong>各包含两个卷积层。</p><p><strong>第一个模块</strong>有64个输出通道，每个后续模块将输出通道数量翻倍，直到该数字达到512</p><p>由于该网络使用8个卷积层和3个全连接层，因此它通常被称为VGG-11</p><p>那么开始搭建一个完整的vgg-11模型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">vgg</span>(<span class="params">conv_arch</span>):</span><br><span class="line">    conv_blks = []</span><br><span class="line">    in_channels = <span class="number">1</span></span><br><span class="line">    <span class="comment"># 卷积层部分</span></span><br><span class="line">    <span class="keyword">for</span> (num_convs, out_channels) <span class="keyword">in</span> conv_arch:</span><br><span class="line">        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))</span><br><span class="line">        in_channels = out_channels</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> nn.Sequential(</span><br><span class="line">        *conv_blks, nn.Flatten(),</span><br><span class="line">        <span class="comment"># 全连接层部分</span></span><br><span class="line">        nn.Linear(out_channels * <span class="number">7</span> * <span class="number">7</span>, <span class="number">4096</span>), nn.ReLU(), nn.Dropout(<span class="number">0.5</span>),</span><br><span class="line">        nn.Linear(<span class="number">4096</span>, <span class="number">4096</span>), nn.ReLU(), nn.Dropout(<span class="number">0.5</span>),</span><br><span class="line">        nn.Linear(<span class="number">4096</span>, <span class="number">10</span>))</span><br><span class="line"></span><br><span class="line">net = vgg(conv_arch)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260502203004.png" alt="450"></p><h4 id="NiN-网络中的网络"><a href="#NiN-网络中的网络" class="headerlink" title="NiN(网络中的网络)"></a>NiN(网络中的网络)</h4><p>AlexNet和VGG对LeNet的改进主要在于如何扩大和加深这两个模块</p><p>然而，如果<strong>使用了全连接层</strong>，可能会<strong>完全放弃表征的空间结构</strong></p><p><em>网络中的网络</em>（<em>NiN</em>）提供了一个非常简单的解决方案：<strong>在每个像素的通道上分别使用多层感知机</strong><br><mark style="background:#fff88f">即将空间维度中的每个像素视为单个样本，将通道维度视为不同特征（feature）</mark></p><p><img src="/images/Pasted%20image%2020260502203606.png" alt="549"></p><p>可以看到，NiN块，是先经过一个普通的卷积层，然后，是两个1x1的卷积层来升降维。<strong>这两个1x1的卷积层，充当带有ReLU激活函数的，逐像素全连接层。</strong></p><p>我们也自己定义一个NiN块看看</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> torch.nn <span class="keyword">as</span> nn</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">nin_block</span>(<span class="params">in_channels, out_channels, kernel_size, strides, padding</span>):</span><br><span class="line"><span class="keyword">return</span> nn.Sequential(</span><br><span class="line">nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),</span><br><span class="line">nn.ReLU(),</span><br><span class="line">nn.Conv2d(out_channels, out_channels, kernel_size=<span class="number">1</span>), nn.ReLU(),</span><br><span class="line">nn.Conv2d(out_channels, out_channels, kernel_size=<span class="number">1</span>), nn.ReLU()</span><br><span class="line">)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">注意，NiN块的最后一个out_channels, 被设置成类别数，这样就相当于对每个像素，做符合类别数量的一个全连接映射。</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>可以看到，他既不升维也不降维，单纯的就是逐像素，把每个通道作为输入，等价于像素位置上的全连接层。很妙啊</p><p>NiN和AlexNet之间的一个<strong>显著区别</strong>是<strong>NiN完全取消了全连接层</strong>，NiN使用一个NiN块，<strong>其输出通道数等于标签类别的数量</strong></p><p>最后放一个_<strong>全局平均汇聚层</strong>_（global average pooling layer），<strong>生成一个对数几率</strong> （logits）：</p><blockquote><p>在深度学习中，<strong>对数几率（Logits）本质上就是一组</strong>没有经过归一化（如 Softmax）的原始得分向量**</p></blockquote><p><img src="/images/Pasted%20image%2020260502205112.png" alt="471"></p><p><mark style="background:#fff88f">NiN 的设计思想</mark>是：与其用复杂且臃肿的全连接层去“猜”每个类别的得分，不如<strong>直接让卷积层产出对应类别的特征图</strong>，然后通过<strong>取平均值</strong>的方式提取出该类别的总体得分。</p><p><strong>这样做最大的好处是显著减少了模型所需参数的数量，因为它彻底取消了占据 AlexNet 大部分参数量的全连接层。</strong></p><h4 id="GoogLeNet-并行连接"><a href="#GoogLeNet-并行连接" class="headerlink" title="GoogLeNet(并行连接)"></a>GoogLeNet(并行连接)</h4><p>GoogLeNet 吸收了NiN的串联网络的思想。<strong>解决的问题是</strong>：<mark style="background:#fff88f">什么样大小的卷积核最合适的问题</mark></p><p>观点是，有时<strong>使用不同大小的卷积核组合是有利的</strong></p><p>GoogLeNet里面，也设计了自己的卷积块：基本的卷积块被称为_<strong>Inception块</strong>_</p><p><img src="/images/Pasted%20image%2020260502205539.png" alt="424"><br>这个块，本身由4条并行路径构成</p><p>中间两条路：先经过1x1卷积，减少输入的通道数，降低模型的复杂性（让后面的卷积核的通道减少）。</p><p>这四条路径，都是用合适的填充让输入与输出的高宽一致（说明这个块的4条路径，都是在通道上做文章，不是在图片尺寸上）</p><p>最后我们将每条线路的输出在通道维度上连结，并构成<strong>Inception块的输出</strong>。</p><p>在Inception块中，通常调整的超参数是<strong>每层输出通道数</strong>（因为是4条路汇聚起来的嘛）</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> torch.nn <span class="keyword">as</span> nn</span><br><span class="line"><span class="keyword">from</span> torch.nn <span class="keyword">import</span> functional <span class="keyword">as</span> F</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Inception</span>(nn.Module):</span><br><span class="line"><span class="comment"># **kwargs表示接收任意多的额外关键字参数</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, in_channles, c1, c2, c3, c4, **kwargs</span>): </span><br><span class="line"><span class="built_in">super</span>().__init__()</span><br><span class="line"><span class="variable language_">self</span>.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=<span class="number">1</span>)</span><br><span class="line"><span class="variable language_">self</span>.p2_1 = nn.Conv2d(in_channels, c2[<span class="number">0</span>], kernel_size=<span class="number">1</span>)</span><br><span class="line"><span class="variable language_">self</span>.p2_2 = nn.Conv2d(c2[<span class="number">0</span>], c2[<span class="number">1</span>], kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>)</span><br><span class="line"><span class="variable language_">self</span>.p3_1 = nn.Conv2d(in_channels, c3[<span class="number">0</span>], kernel_size=<span class="number">1</span>)</span><br><span class="line"><span class="variable language_">self</span>.p3_2 = nn.Conv2d(c3[<span class="number">0</span>], c3[<span class="number">1</span>], kernel_size=<span class="number">5</span>, padding=<span class="number">2</span>)</span><br><span class="line"><span class="variable language_">self</span>.p4_1 = nn.MaxPoll2d(kernel_size=<span class="number">3</span>, stride=<span class="number">1</span>, padding=<span class="number">1</span>)</span><br><span class="line"><span class="variable language_">self</span>.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x</span>):</span><br><span class="line">p1 = F.relu(<span class="variable language_">self</span>.p1_1(x))</span><br><span class="line">p2 = F.relu(<span class="variable language_">self</span>.p2_2(F.relu(<span class="variable language_">self</span>.p2_1(x))))</span><br><span class="line">p3 = F.relu(<span class="variable language_">self</span>.p3_2(F.relu(<span class="variable language_">self</span>.p3_1(x))))</span><br><span class="line">p4 = F.relu(<span class="variable language_">self</span>.p4_2(<span class="variable language_">self</span>.p4_1(x)))</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> torch.cat((p1, p2, p3, p4), dim=<span class="number">1</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>GoogLeNet之所以有效，是因为用来不同尺寸的滤波器去探索图像。为不同的滤波器分配不同数量的参数。</p><p>下面看看完整的GoogLeNet模型架构<br><img src="/images/Pasted%20image%2020260502211318.png" alt="150"></p><p>一共使用<strong>9个Inception块</strong>和<strong>全局平均汇聚层</strong>的堆叠来生成其估计值</p><p>Inception块之间的<strong>最大汇聚层</strong>可<strong>降低维度</strong></p><p><strong>第一个模块类似于AlexNet和LeNet</strong></p><p>Inception块的<strong>组合从VGG继承</strong>（因为他有多个inception块相连接）<br><img src="/images/Pasted%20image%2020260502211843.png"><br><img src="/images/Pasted%20image%2020260502211858.png" alt="510"></p><p><img src="/images/Pasted%20image%2020260502211917.png" alt="478"></p><p><img src="/images/Pasted%20image%2020260502211937.png" alt="488"></p><p>下面完整实现这个GoogLeNet网络模型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">b1 = nn.Sequential(nn.Conv2d(<span class="number">1</span>, <span class="number">64</span>, kernel_size=<span class="number">7</span>, stride=<span class="number">2</span>, padding=<span class="number">3</span>),</span><br><span class="line">                   nn.ReLU(),</span><br><span class="line">                   nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))</span><br><span class="line">                   </span><br><span class="line">b2 = nn.Sequential(nn.Conv2d(<span class="number">64</span>, <span class="number">64</span>, kernel_size=<span class="number">1</span>),</span><br><span class="line">                   nn.ReLU(),</span><br><span class="line">                   nn.Conv2d(<span class="number">64</span>, <span class="number">192</span>, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>),</span><br><span class="line">                   nn.ReLU(),</span><br><span class="line">                   nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))   </span><br><span class="line">                               </span><br><span class="line">b3 = nn.Sequential(Inception(<span class="number">192</span>, <span class="number">64</span>, (<span class="number">96</span>, <span class="number">128</span>), (<span class="number">16</span>, <span class="number">32</span>), <span class="number">32</span>),</span><br><span class="line">                   Inception(<span class="number">256</span>, <span class="number">128</span>, (<span class="number">128</span>, <span class="number">192</span>), (<span class="number">32</span>, <span class="number">96</span>), <span class="number">64</span>),</span><br><span class="line">                   nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))                  </span><br><span class="line">b4 = nn.Sequential(Inception(<span class="number">480</span>, <span class="number">192</span>, (<span class="number">96</span>, <span class="number">208</span>), (<span class="number">16</span>, <span class="number">48</span>), <span class="number">64</span>),</span><br><span class="line">                   Inception(<span class="number">512</span>, <span class="number">160</span>, (<span class="number">112</span>, <span class="number">224</span>), (<span class="number">24</span>, <span class="number">64</span>), <span class="number">64</span>),</span><br><span class="line">                   Inception(<span class="number">512</span>, <span class="number">128</span>, (<span class="number">128</span>, <span class="number">256</span>), (<span class="number">24</span>, <span class="number">64</span>), <span class="number">64</span>),</span><br><span class="line">                   Inception(<span class="number">512</span>, <span class="number">112</span>, (<span class="number">144</span>, <span class="number">288</span>), (<span class="number">32</span>, <span class="number">64</span>), <span class="number">64</span>),</span><br><span class="line">                   Inception(<span class="number">528</span>, <span class="number">256</span>, (<span class="number">160</span>, <span class="number">320</span>), (<span class="number">32</span>, <span class="number">128</span>), <span class="number">128</span>),</span><br><span class="line">                   nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))</span><br><span class="line">                   </span><br><span class="line">b5 = nn.Sequential(Inception(<span class="number">832</span>, <span class="number">256</span>, (<span class="number">160</span>, <span class="number">320</span>), (<span class="number">32</span>, <span class="number">128</span>), <span class="number">128</span>),</span><br><span class="line">                   Inception(<span class="number">832</span>, <span class="number">384</span>, (<span class="number">192</span>, <span class="number">384</span>), (<span class="number">48</span>, <span class="number">128</span>), <span class="number">128</span>),</span><br><span class="line">                   nn.AdaptiveAvgPool2d((<span class="number">1</span>,<span class="number">1</span>)),</span><br><span class="line">                   nn.Flatten())</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(<span class="number">1024</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><h4 id="批量规范化"><a href="#批量规范化" class="headerlink" title="批量规范化"></a>批量规范化</h4><p>训练深层神经网络是十分困难的，特别是在<strong>较短的时间内使他们收敛</strong>更加棘手。 本节将介绍_<strong>批量规范化</strong>_（batch normalization）</p><p>可持续加速深层网络的收敛速度（后面再结合残差块设计）</p><p>BatchNormalization，能够训练100层以上的网络</p><p><mark style="background:#affad1">训练深层网络的需要</mark></p><ul><li>使用真实数据时，我们的第一步是<strong>标准化输入特征</strong>，使其平均值为0，方差为1。这种标准化可以很好地与我们的<strong>优化器配合使用</strong>，因为它可以<strong>将参数的量级进行统一</strong></li><li>对于典型的MLP和CNN，训练时的中间层变量，可能具有更广的变化范围。</li></ul><p> 批量规范化的发明者<strong>非正式地假设</strong>，这些变量<strong>分布中的这种偏移</strong>可能<strong>会阻碍网络的收敛</strong>。 直观地说，我们可能会猜想，如果一个层的可变值是另一层的100倍，这可能需要对学习率进行补偿调整</p><ul><li>更深层的网络很复杂，容易过拟合。 这意味着正则化变得更加重要。</li></ul><p><mark style="background:#fff88f">批量规范化应用于单个可选层（也可以应用到所有层）</mark><br><strong>其原理如下</strong>：</p><ul><li>在每次训练迭代中，我们首先<strong>规范化输入</strong>，即通过<font color="#00b050">减去其均值并除以其标准差，其中两者均基于当前小批量处理</font>。 </li><li>接下来，我们<strong>应用比例系数和比例偏移</strong>。 正是由于这个基于_批量_统计的_标准化_，才有了_批量规范化_的名称。</li></ul><blockquote><p>请注意，如果我们尝试使用大小为1的小批量应用批量规范化，我们将无法学到任何东西。 这是因为在减去均值之后，每个隐藏单元将为0</p></blockquote><p><mark style="background:#fff88f">所以，只有使用足够大的小批量，批量规范化这种方法才是有效且稳定的</mark><br>请注意，在应用批量规范化时，<strong>批量大小的选择</strong>可能<strong>比没有批量规范化时更重要</strong></p><p>简单说，批量规范化，主要有两个部分：<br><img src="/images/Pasted%20image%2020260502232519.png" alt="534"><br>这里的批量规范化层，引入了两个模型参数：<strong>γ（拉伸参数）</strong>，<strong>β（偏移参数）</strong></p><p>批量规范化（Batch Normalization，简称 BN）层确实是一个独立的“层”，它的核心作用就是<strong>规范化它前面那一层的输出（也就是后面那一层的输入）</strong>。</p><p><img src="/images/Pasted%20image%2020260502233253.png" alt="534"></p><p><img src="/images/Pasted%20image%2020260502233321.png" alt="512"></p><p><strong>计算样本均值和方差的小巧思</strong><br><img src="/images/Pasted%20image%2020260502233427.png" alt="605"></p><p><img src="/images/Pasted%20image%2020260502233519.png" alt="642"></p><h5 id="批量规范化层实现"><a href="#批量规范化层实现" class="headerlink" title="批量规范化层实现"></a>批量规范化层实现</h5><p>因为批量规范化层，肯定需要知道一个batch的所有内容，所以，不能像其他层那样忽略batch_size的大小。</p><p><mark style="background:#affad1">全连接层的批量规范化</mark><br>我们将批量规范化层置于全连接层中的仿射变换（预激活值）和激活函数之间<br><img src="/images/Pasted%20image%2020260502233736.png"></p><p><mark style="background:#affad1">卷积层的批量规范化</mark><br>我们可以在卷积层之后和非线性激活函数之前应用批量规范化</p><p>卷积有<strong>多个输出通道</strong>时，我们需要对<strong>这些通道的“每个”输出执行批量规范化</strong>，每个通道<strong>都有自己的拉伸（scale）和偏移（shift）参数</strong>，这两个参数都是标量</p><p><img src="/images/Pasted%20image%2020260502233934.png"><br>具体的计算方式为：以通道为单位算规范化<br><strong>针对mxpxq的元素进行参与计算（m &#x3D; batch_size）</strong><br><img src="/images/Pasted%20image%2020260502234326.png" alt="321"></p><p>所以，每个卷积核通道，持有一组批量规范化的参数。</p><p><mark style="background:#affad1">前向推理过程中的批量规范化</mark><br>批量规范化在训练模式和预测模式下的行为通常不同</p><p><img src="/images/Pasted%20image%2020260502234736.png" alt="497"></p><p><mark style="background:#affad1">批量规范化层的实现</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">batch_norm</span>(<span class="params">X, gamma, beta, moving_mean, moving_var, eps, momentum</span>):</span><br><span class="line">    <span class="comment"># 通过is_grad_enabled来判断当前模式是训练模式还是预测模式</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> torch.is_grad_enabled():</span><br><span class="line">        <span class="comment"># 如果是在预测模式下，直接使用传入的移动平均所得的均值和方差</span></span><br><span class="line">        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">assert</span> <span class="built_in">len</span>(X.shape) <span class="keyword">in</span> (<span class="number">2</span>, <span class="number">4</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(X.shape) == <span class="number">2</span>:</span><br><span class="line">            <span class="comment"># 使用全连接层的情况，计算特征维上的均值和方差</span></span><br><span class="line">            mean = X.mean(dim=<span class="number">0</span>)</span><br><span class="line">            var = ((X - mean) ** <span class="number">2</span>).mean(dim=<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="comment"># 使用二维卷积层的情况，计算通道维上（axis=1）的均值和方差。</span></span><br><span class="line">            <span class="comment"># 这里我们需要保持X的形状以便后面可以做广播运算</span></span><br><span class="line">            mean = X.mean(dim=(<span class="number">0</span>, <span class="number">2</span>, <span class="number">3</span>), keepdim=<span class="literal">True</span>)</span><br><span class="line">            var = ((X - mean) ** <span class="number">2</span>).mean(dim=(<span class="number">0</span>, <span class="number">2</span>, <span class="number">3</span>), keepdim=<span class="literal">True</span>)</span><br><span class="line">        <span class="comment"># 训练模式下，用当前的均值和方差做标准化</span></span><br><span class="line">        X_hat = (X - mean) / torch.sqrt(var + eps)</span><br><span class="line">        <span class="comment"># 更新移动平均的均值和方差</span></span><br><span class="line">        moving_mean = momentum * moving_mean + (<span class="number">1.0</span> - momentum) * mean</span><br><span class="line">        moving_var = momentum * moving_var + (<span class="number">1.0</span> - momentum) * var</span><br><span class="line">    Y = gamma * X_hat + beta  <span class="comment"># 缩放和移位</span></span><br><span class="line">    <span class="keyword">return</span> Y, moving_mean.data, moving_var.data</span><br></pre></td></tr></table></figure><ol><li>维度判断：处理全连接层输出</li></ol><ul><li><code>assert len(X.shape) in (2, 4)</code>：这行代码是在确认输入的形状。在深度学习中，全连接层的输出通常是二维张量，形状为 <code>(批量大小, 特征维度)</code>。</li><li><code>if len(X.shape) == 2:</code>：当进入这个分支时，说明模型当前正在对全连接层的输出进行规范化。</li></ul><ol><li>计算特征维上的统计量<br>在全连接层中，批量规范化是针对每一个特征（隐藏单元）独立进行的。</li></ol><ul><li><strong>均值计算 (<code>mean = X.mean(dim=0)</code>)</strong>：<ul><li><code>dim=0</code> 表示沿着“批量（Batch）”这个维度进行计算。</li><li>这意味着，如果你有 128 个样本，每个样本有 64 个特征，那么它会计算这 128 个样本在每一个特征位置上的平均值。</li><li>最终得到的 <code>mean</code> 形状与特征维度一致（例如长度为 64 的向量）。</li></ul></li><li><strong>方差计算 (<code>var = ((X - mean) 2).mean(dim=0)</code>)</strong>：<ul><li>这行代码计算的是样本方差。</li><li>它同样是跨样本（<code>dim=0</code>）进行的，用来衡量该小批量中每个特征的数据离散程度。</li></ul></li></ul><hr><p>在处理二维卷积层的输出（形状通常为 <code>(Batch, Channel, Height, Width)</code>）时，它的计算方式如下：</p><ol><li>为什么是 <code>dim=(0, 2, 3)</code>？<br>这是最关键的部分。在卷积层的批量规范化中，我们需要跨越<strong>样本</strong>和<strong>空间位置</strong>来收集数据：</li></ol><ul><li><strong><code>0</code> (Batch)</strong>：跨越小批量中的所有样本。</li><li><strong><code>2</code> (Height)</strong> 和 <strong><code>3</code> (Width)</strong>：跨越图像的所有空间坐标。</li><li><strong>结果</strong>：计算出的 <code>mean</code> 和 <code>var</code> 是针对<strong>每一个通道</strong>唯一的标量。这意味着同一个通道内所有的像素点，无论在哪个样本、哪个位置，都共用这一组均值和方差。</li></ul><ol><li><code>keepdim=True</code> 的作用<br>正如注释所言，这是为了后续的<strong>广播运算（Broadcasting）</strong>：</li></ol><ul><li>如果输出通道是 $16$，不加这个参数计算出的 <code>mean</code> 形状可能是 <code>(16,)</code>。</li><li>加上 <code>keepdim=True</code> 后，<code>mean</code> 的形状会保持为 <code>(1, 16, 1, 1)</code>。</li><li>这样在执行 <code>X - mean</code> 时，PyTorch 会自动把这个 $1 \times 16 \times 1 \times 1$ 的张量“扩展”到和原始 <code>X</code> 一样的形状，从而实现对每个通道内所有像素点进行统一减法的操作。</li></ul><hr><ol><li>标准化（Standardization）</li></ol><figure class="highlight arcade"><table><tr><td class="code"><pre><span class="line">X_hat = (X - <span class="built_in">mean</span>) / torch.<span class="built_in">sqrt</span>(<span class="keyword">var</span> + eps)</span><br></pre></td></tr></table></figure><ul><li><strong>减去均值与除以标准差</strong>：这行代码实现了你之前提到的“主动居中”。它将当前通道或特征的分布强制变换为均值为 0、方差为 1 的标准分布。</li><li><strong><code>eps</code> (Epsilon) 的作用</strong>：对应于你参考资料中提到的“在方差估计值中添加一个小的常量 $\epsilon &gt; 0$”。其目的是确保分母不为零，防止在方差接近 0 时计算崩溃。</li></ul><ol start="2"><li>更新移动平均（Updating Moving Average）</li></ol><figure class="highlight apache"><table><tr><td class="code"><pre><span class="line"><span class="attribute">moving_mean</span> = momentum * momentum * moving_mean + (<span class="number">1</span>.<span class="number">0</span> - momentum) * mean</span><br><span class="line"><span class="attribute">moving_var</span> = momentum * momentum * moving_var + (<span class="number">1</span>.<span class="number">0</span> - momentum) * var</span><br></pre></td></tr></table></figure><p>这是 BN 层最“聪明”的地方，它在训练时偷偷为预测模式（Inference）做准备：</p><ul><li><strong>为什么要更新？</strong>：正如你查阅的“预测过程中的批量规范化”所述，预测时可能一次只输入一个样本，无法实时算方差。因此，我们需要通过移动平均来估算<strong>整个训练数据集</strong>的全局均值和方差。</li><li><strong><code>momentum</code> (动量)</strong>：它决定了“记忆”的权重。<ul><li>通常设为一个接近 1 的值（如 0.9 或 0.99）。</li><li>这意味着新的全局估算值 &#x3D; <strong>90% 的旧记忆</strong> + <strong>10% 的当前小批量观察值</strong>。</li></ul></li><li><strong>平滑噪声</strong>：训练过程中的单个小批量均值（$\hat{\mu}_B$）含有随机噪声。通过这种加权平均，模型可以抵消消缩放问题，得到一个平滑、确定的全局统计量，供预测时使用。</li></ul><hr><p>通常情况下，我们用一个单独的函数定义其数学原理，比如说<code>batch_norm</code>。 然后，我们将此功能集成到一个自定义层中，其代码主要处理数据移动到训练设备（如GPU）、分配和初始化任何必需的变量、跟踪移动平均线（此处为均值和方差）等问题</p><p>这个只是实现了批量规范化的计算，但是移动均值，移动方差的存储这些，还是需要一个具体的类来存储</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BatchNorm</span>(nn.Module):</span><br><span class="line">    <span class="comment"># num_features：完全连接层的输出数量或卷积层的输出通道数。</span></span><br><span class="line">    <span class="comment"># num_dims：2表示完全连接层，4表示卷积层</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, num_features, num_dims</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="keyword">if</span> num_dims == <span class="number">2</span>:</span><br><span class="line">            shape = (<span class="number">1</span>, num_features)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            shape = (<span class="number">1</span>, num_features, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        <span class="comment"># 参与求梯度和迭代的拉伸和偏移参数，分别初始化成1和0</span></span><br><span class="line">        <span class="variable language_">self</span>.gamma = nn.Parameter(torch.ones(shape))</span><br><span class="line">        <span class="variable language_">self</span>.beta = nn.Parameter(torch.zeros(shape))</span><br><span class="line">        <span class="comment"># 非模型参数的变量初始化为0和1</span></span><br><span class="line">        <span class="variable language_">self</span>.moving_mean = torch.zeros(shape)</span><br><span class="line">        <span class="variable language_">self</span>.moving_var = torch.ones(shape)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        <span class="comment"># 如果X不在内存上，将moving_mean和moving_var</span></span><br><span class="line">        <span class="comment"># 复制到X所在显存上</span></span><br><span class="line">        <span class="keyword">if</span> <span class="variable language_">self</span>.moving_mean.device != X.device:</span><br><span class="line">            <span class="variable language_">self</span>.moving_mean = <span class="variable language_">self</span>.moving_mean.to(X.device)</span><br><span class="line">            <span class="variable language_">self</span>.moving_var = <span class="variable language_">self</span>.moving_var.to(X.device)</span><br><span class="line">        <span class="comment"># 保存更新过的moving_mean和moving_var</span></span><br><span class="line">        Y, <span class="variable language_">self</span>.moving_mean, <span class="variable language_">self</span>.moving_var = batch_norm(</span><br><span class="line">            X, <span class="variable language_">self</span>.gamma, <span class="variable language_">self</span>.beta, <span class="variable language_">self</span>.moving_mean,</span><br><span class="line">            <span class="variable language_">self</span>.moving_var, eps=<span class="number">1e-5</span>, momentum=<span class="number">0.9</span>)</span><br><span class="line">        <span class="keyword">return</span> Y</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">使用批量规范化层的LeNet</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">net = nn.Sequential(</span><br><span class="line">    nn.Conv2d(<span class="number">1</span>, <span class="number">6</span>, kernel_size=<span class="number">5</span>), BatchNorm(<span class="number">6</span>, num_dims=<span class="number">4</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>),</span><br><span class="line">    nn.Conv2d(<span class="number">6</span>, <span class="number">16</span>, kernel_size=<span class="number">5</span>), BatchNorm(<span class="number">16</span>, num_dims=<span class="number">4</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>), nn.Flatten(),</span><br><span class="line">    nn.Linear(<span class="number">16</span>*<span class="number">4</span>*<span class="number">4</span>, <span class="number">120</span>), BatchNorm(<span class="number">120</span>, num_dims=<span class="number">2</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">120</span>, <span class="number">84</span>), BatchNorm(<span class="number">84</span>, num_dims=<span class="number">2</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">84</span>, <span class="number">10</span>))</span><br><span class="line">    </span><br><span class="line"><span class="string">&quot;&quot;&quot;训练代码&quot;&quot;&quot;</span></span><br><span class="line">lr, num_epochs, batch_size = <span class="number">1.0</span>, <span class="number">10</span>, <span class="number">256</span></span><br><span class="line">train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)</span><br><span class="line">d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">pytorch实现的batchNorm</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">net = nn.Sequential(</span><br><span class="line">    nn.Conv2d(<span class="number">1</span>, <span class="number">6</span>, kernel_size=<span class="number">5</span>), nn.BatchNorm2d(<span class="number">6</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>),</span><br><span class="line">    nn.Conv2d(<span class="number">6</span>, <span class="number">16</span>, kernel_size=<span class="number">5</span>), nn.BatchNorm2d(<span class="number">16</span>), nn.Sigmoid(),</span><br><span class="line">    nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>), nn.Flatten(),</span><br><span class="line">    nn.Linear(<span class="number">256</span>, <span class="number">120</span>), nn.BatchNorm1d(<span class="number">120</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">120</span>, <span class="number">84</span>), nn.BatchNorm1d(<span class="number">84</span>), nn.Sigmoid(),</span><br><span class="line">    nn.Linear(<span class="number">84</span>, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><h4 id="残差网络（ResNet）"><a href="#残差网络（ResNet）" class="headerlink" title="残差网络（ResNet）"></a>残差网络（ResNet）</h4><p>当我们的网络越来越深之后，新添加的层，如何提升网络性能 -&gt; <strong>残差块</strong></p><p><mark style="background:#affad1">解释残差块的设计原理</mark></p><p><strong>$f$ 与 $\mathcal{F}$</strong>：$\mathcal{F}$ 代表<strong>函数族</strong>（即你选择的模型架构，如一个 5 层的卷积神经网络）。$f$ 是这个族群中的某一个具体的函数（即一组特定的权重参数）。</p><p><strong>$L(\mathbf{X}, \mathbf{y}, f)$</strong>：这是<strong>损失函数</strong>（Loss Function）。它衡量了使用函数 $f$ 对特性 $\mathbf{X}$ 进行预测时，预测结果与真实标签 $\mathbf{y}$ 之间的差距。</p><p><strong>f</strong>是我们实际用数据训练出来的网络，但是假设我们实际真正想要的是<strong>f</strong>*，但是一般我们没办法找到</p><p>所有我们尝试找到一个<strong>f’</strong>, 他是我们在<strong>F</strong>中的最佳选择。</p><p>所以，我们实际面临的问题是这样子的：</p><ul><li>给定数据集（X，y），这个数据集，可以训练出所有的f</li><li>我们期望找到f’， 他是能在（X，y）上的Loss损失min的那一个f</li></ul><p><img src="/images/Pasted%20image%2020260503093620.png"></p><p>所以，f’ 和 f，都∈F， 也就是说，他们有一样的model结构，只是参数不同而已。</p><p>那么如何得到最好的f’呢？唯一的可能性，就是重新设计一个更强的F’架构，才能从根本上解决这个问题。（毕竟天花板摆在那里）</p><p><strong>但是新架构F’无法保证新体系能够完全涵盖F的覆盖域，也可能更糟糕</strong>。<br><img src="/images/Pasted%20image%2020260503094227.png" alt="518"></p><p><mark style="background:#fff88f">因此，只有当较复杂的函数类包含较小的函数类时，我们才能确保提高它们的性能</mark></p><p>所以<strong>我们的目的</strong>，<mark style="background:#fff88f">就是找到一种修改优化模型结构的方法，能确保新的模型结构F’， 能完全包含旧模型结构F的覆盖域</mark></p><p>也就是说，对于深度神经网络，如果我们能将<strong>新添加的层</strong>，训练成恒等映射<strong>f(x)&#x3D;x</strong>, 那么就能<strong>确保F‘肯定包含F这件事</strong>， 此时，由新模型结构F’ 才可能得出更优的解来拟合训练数据集。</p><p>这就是残差网络ResNet</p><p><strong>残差网络的核心思想是</strong>：<mark style="background:#fff88f">每个附加层都应该更容易地包含原始函数作为其元素之一</mark></p><p>看看<strong>残差块</strong>，是如何实现这个思想的</p><p><img src="/images/c6d8d2e1a42e620b065fe92c6480c344.jpg" alt="606"></p><p><img src="/images/Pasted%20image%2020260503100136.png" alt="378"></p><p><strong>以上展示的残差块，就是我们为了改造F，新增的一个层，变成F‘</strong></p><p>ResNet沿用VGG，残差块里首先有2个有相同输出通道数的3x3卷积层，每个卷积层后接一个批量规范化层和ReLU激活函数<br><img src="/images/6d2d9038626cf8688583c567c0df0deb.jpg" alt="503"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> torch.nn <span class="keyword">import</span> functional <span class="keyword">as</span> F</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Residual</span>(nn.Module):  <span class="comment">#@save</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, input_channels, num_channels,</span></span><br><span class="line"><span class="params">                 use_1x1conv=<span class="literal">False</span>, strides=<span class="number">1</span></span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.conv1 = nn.Conv2d(input_channels, num_channels,</span><br><span class="line">                               kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>, stride=strides)</span><br><span class="line">        <span class="variable language_">self</span>.conv2 = nn.Conv2d(num_channels, num_channels,</span><br><span class="line">                               kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> use_1x1conv:</span><br><span class="line">            <span class="variable language_">self</span>.conv3 = nn.Conv2d(input_channels, num_channels,</span><br><span class="line">                                   kernel_size=<span class="number">1</span>, stride=strides)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="variable language_">self</span>.conv3 = <span class="literal">None</span></span><br><span class="line">        <span class="variable language_">self</span>.bn1 = nn.BatchNorm2d(num_channels)</span><br><span class="line">        <span class="variable language_">self</span>.bn2 = nn.BatchNorm2d(num_channels)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        Y = F.relu(<span class="variable language_">self</span>.bn1(<span class="variable language_">self</span>.conv1(X)))</span><br><span class="line">        Y = <span class="variable language_">self</span>.bn2(<span class="variable language_">self</span>.conv2(Y))</span><br><span class="line">        <span class="keyword">if</span> <span class="variable language_">self</span>.conv3:</span><br><span class="line">            X = <span class="variable language_">self</span>.conv3(X)</span><br><span class="line">        Y += X</span><br><span class="line">        <span class="keyword">return</span> F.relu(Y)</span><br></pre></td></tr></table></figure><p>此代码生成两种类型的网络： 一种是当<code>use_1x1conv=False</code>时，应用ReLU非线性函数之前，将输入添加到输出。 另一种是当<code>use_1x1conv=True</code>时，添加通过1x1卷积调整通道和分辨率</p><p><img src="/images/Pasted%20image%2020260503100920.png" alt="521"><br>之所以在残差块（Residual Block）的旁路（Shortcut）使用它能减半高和宽，秘密不在于<strong>卷积核的大小</strong>，而是在于卷积操作的一个关键参数：<strong>步幅（Stride）</strong></p><p>ResNet除了使用残差块的设计外，还由一个不同之处是，每个卷积层后面，都增加了批量规范化。</p><p>下面我们来用上面定义好的一个残差块，构建我们的resnet块</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">b1 = nn.Sequential(nn.Conv2d(<span class="number">1</span>, <span class="number">64</span>, kernel_size=<span class="number">7</span>, stride=<span class="number">2</span>, padding=<span class="number">3</span>),</span><br><span class="line">                   nn.BatchNorm2d(<span class="number">64</span>), nn.ReLU(),</span><br><span class="line">                   nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">resnet_block</span>(<span class="params">input_channels, num_channels, num_residuals,</span></span><br><span class="line"><span class="params">                 first_block=<span class="literal">False</span></span>):</span><br><span class="line">    blk = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(num_residuals):</span><br><span class="line">        <span class="keyword">if</span> i == <span class="number">0</span> <span class="keyword">and</span> <span class="keyword">not</span> first_block:</span><br><span class="line">            blk.append(Residual(input_channels, num_channels,</span><br><span class="line">                                use_1x1conv=<span class="literal">True</span>, strides=<span class="number">2</span>))</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            blk.append(Residual(num_channels, num_channels))</span><br><span class="line">    <span class="keyword">return</span> blk</span><br><span class="line"></span><br><span class="line"><span class="comment"># nn.Sequential(Residual_1, Residual_2)</span></span><br><span class="line">b2 = nn.Sequential(*resnet_block(<span class="number">64</span>, <span class="number">64</span>, <span class="number">2</span>, first_block=<span class="literal">True</span>)) </span><br><span class="line">b3 = nn.Sequential(*resnet_block(<span class="number">64</span>, <span class="number">128</span>, <span class="number">2</span>))</span><br><span class="line">b4 = nn.Sequential(*resnet_block(<span class="number">128</span>, <span class="number">256</span>, <span class="number">2</span>))</span><br><span class="line">b5 = nn.Sequential(*resnet_block(<span class="number">256</span>, <span class="number">512</span>, <span class="number">2</span>))</span><br><span class="line"></span><br><span class="line">net = nn.Sequential(b1, b2, b3, b4, b5,</span><br><span class="line">                    nn.AdaptiveAvgPool2d((<span class="number">1</span>,<span class="number">1</span>)),</span><br><span class="line">                    nn.Flatten(), nn.Linear(<span class="number">512</span>, <span class="number">10</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">X = torch.rand(size=(<span class="number">1</span>, <span class="number">1</span>, <span class="number">224</span>, <span class="number">224</span>))</span><br><span class="line"><span class="keyword">for</span> layer <span class="keyword">in</span> net:</span><br><span class="line">    X = layer(X)</span><br><span class="line">    <span class="built_in">print</span>(layer.__class__.__name__,<span class="string">&#x27;output shape:\t&#x27;</span>, X.shape)</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">Sequential output shape:     torch.Size([1, 64, 56, 56])</span></span><br><span class="line"><span class="string">Sequential output shape:     torch.Size([1, 64, 56, 56])</span></span><br><span class="line"><span class="string">Sequential output shape:     torch.Size([1, 128, 28, 28])</span></span><br><span class="line"><span class="string">Sequential output shape:     torch.Size([1, 256, 14, 14])</span></span><br><span class="line"><span class="string">Sequential output shape:     torch.Size([1, 512, 7, 7])</span></span><br><span class="line"><span class="string">AdaptiveAvgPool2d output shape:      torch.Size([1, 512, 1, 1])</span></span><br><span class="line"><span class="string">Flatten output shape:        torch.Size([1, 512])</span></span><br><span class="line"><span class="string">Linear output shape:         torch.Size([1, 10])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260503102001.png"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">lr, num_epochs, batch_size = <span class="number">0.05</span>, <span class="number">10</span>, <span class="number">256</span></span><br><span class="line">train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=<span class="number">96</span>)</span><br><span class="line">d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">loss 0.012, train acc 0.997, test acc 0.893</span></span><br><span class="line"><span class="string">5032.7 examples/sec on cuda:0</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><h4 id="稠密层（DenseNet）"><a href="#稠密层（DenseNet）" class="headerlink" title="稠密层（DenseNet）"></a>稠密层（DenseNet）</h4><p>ResNet极大地改变了如何参数化深层网络中函数的观点</p><p><strong>_稠密连接网络_在某种程度上是ResNet的逻辑扩展</strong></p><p>我们曾经，对任意函数进行泰勒展开，可以分解成很多项<br><img src="/images/Pasted%20image%2020260503102305.png" alt="357"></p><p>同样ResNet将函数展开：<br><img src="/images/Pasted%20image%2020260503102331.png" alt="137"><br>可以看到，ResNet网络，本身就是将这个残差块的映射f(x), 分成两条路，<strong>一路x旁路</strong>，<strong>一路是差异g(x)</strong>，即一<strong>个简单的线性项</strong>和<strong>一个复杂的非线性项</strong>（可以想象成<mark style="background:#fff88f">泰勒展开的余项</mark>）</p><p>那么，再拓展一步，如果我们想要把f(x)再像泰勒展开一样再进一步展开呢，你看线性x，就是泰勒展开已经展开的部分，非线性项g(x), 就是余项，可以进一步展开。这个方案就是DenseNet</p><p><img src="/images/Pasted%20image%2020260503102722.png" alt="558"></p><p>ResNet和DenseNet的关键区别在于，DenseNet输出是_连接_（用图中的[.]表示）而不是如ResNet的简单相加</p><p>所以，如果我们的这个残差块，要学习的真的是一个很复杂的映射关系，那么<br><img src="/images/Pasted%20image%2020260503102909.png" alt="610"></p><p><img src="/images/Pasted%20image%2020260503103021.png" alt="576"><br><img src="/images/Pasted%20image%2020260503103225.png" alt="577"><br><img src="/images/Pasted%20image%2020260503103213.png" alt="448"></p><p><mark style="background:#fff88f">所以，稠密层里面，更高阶的余项，不是像泰勒展开那样，去不停的细分，而是说，x是基础特征，f1是基于x挖掘的一阶特征，f2是利用f1,x 组合出来的更复杂的特征，….， fn是最高级的特征</mark></p><p><strong>泰勒展开是“横向细分”，DenseNet 是“纵向抽象”</strong></p><p><img src="/images/Pasted%20image%2020260503103616.png" alt="580"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">conv_block</span>(<span class="params">input_channels, num_channels</span>):</span><br><span class="line">    <span class="keyword">return</span> nn.Sequential(</span><br><span class="line">        nn.BatchNorm2d(input_channels), nn.ReLU(),</span><br><span class="line">        nn.Conv2d(input_channels, num_channels, kernel_size=<span class="number">3</span>, padding=<span class="number">1</span>))</span><br><span class="line">        </span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DenseBlock</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, num_convs, input_channels, num_channels</span>):</span><br><span class="line">        <span class="built_in">super</span>(DenseBlock, <span class="variable language_">self</span>).__init__()</span><br><span class="line">        layer = []</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(num_convs):</span><br><span class="line">            layer.append(conv_block(</span><br><span class="line">                num_channels * i + input_channels, num_channels))</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        x: i</span></span><br><span class="line"><span class="string">        f1(x): i -&gt; n</span></span><br><span class="line"><span class="string">        f2(x,f1): i+n -&gt; n</span></span><br><span class="line"><span class="string">        f3(x,f1,f2): i+n+n -&gt; n</span></span><br><span class="line"><span class="string">        f4(x,f1,f2,f3): i+n+n+n -&gt; n</span></span><br><span class="line"><span class="string">        ...</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>.net = nn.Sequential(*layer)</span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        net(x): i-&gt;n</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X</span>):</span><br><span class="line">        <span class="keyword">for</span> blk <span class="keyword">in</span> <span class="variable language_">self</span>.net:</span><br><span class="line">            Y = blk(X)</span><br><span class="line">            <span class="comment"># 连接通道维度上每个块的输入和输出</span></span><br><span class="line">            X = torch.cat((X, Y), dim=<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">return</span> X</span><br><span class="line">        </span><br><span class="line">        </span><br><span class="line">blk = DenseBlock(<span class="number">2</span>, <span class="number">3</span>, <span class="number">10</span>)</span><br><span class="line">X = torch.randn(<span class="number">4</span>, <span class="number">3</span>, <span class="number">8</span>, <span class="number">8</span>)</span><br><span class="line">Y = blk(X)</span><br><span class="line">Y.shape</span><br></pre></td></tr></table></figure><p><img src="/images/ead1cafb6e45082db19a94962282f0ed.jpg" alt="570"></p><p>上面就是一个<mark style="background:#fff88f">稠密块的结构</mark>。(两个卷积层，输入维度3， 输出维度10)</p><p>这里的输入通道，指的是x的维度，输出通道，是指的每个卷积层的输出通道，但是整个稠密层，其实是输出所有的卷积的结果，包括原始的结果。</p><p>所有实际上，一个稠密块的输出，要这么看，输入x, 输出（x, f1(x),f2(x,f1(x))）<br><img src="/images/d6eb731199bdadece5cb8dad8bcc69fa.jpg" alt="550"></p><p>由于<strong>每个稠密块</strong>都会带来<strong>通道数的增加</strong>，使用过多则会过于<strong>复杂化模型</strong></p><p>而<strong>过渡层</strong>可以用来<strong>控制模型复杂度</strong>（通过1x1的卷积层来减小通道数，降维，然后使用步幅为2的平均池化层来减半尺寸，进一步降低模型复杂度）</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">transition_block</span>(<span class="params">input_channels, num_channels</span>):</span><br><span class="line">    <span class="keyword">return</span> nn.Sequential(</span><br><span class="line">        nn.BatchNorm2d(input_channels), nn.ReLU(),</span><br><span class="line">        nn.Conv2d(input_channels, num_channels, kernel_size=<span class="number">1</span>),</span><br><span class="line">        nn.AvgPool2d(kernel_size=<span class="number">2</span>, stride=<span class="number">2</span>))</span><br><span class="line">        </span><br><span class="line">blk = transition_block(<span class="number">23</span>, <span class="number">10</span>)</span><br><span class="line">blk(Y).shape</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">torch.Size([4, 10, 4, 4])</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>下面来构建具体的DenseNet模型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">b1 = nn.Sequential(</span><br><span class="line">    nn.Conv2d(<span class="number">1</span>, <span class="number">64</span>, kernel_size=<span class="number">7</span>, stride=<span class="number">2</span>, padding=<span class="number">3</span>),</span><br><span class="line">    nn.BatchNorm2d(<span class="number">64</span>), nn.ReLU(),</span><br><span class="line">    nn.MaxPool2d(kernel_size=<span class="number">3</span>, stride=<span class="number">2</span>, padding=<span class="number">1</span>))</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line"><span class="comment"># num_channels为当前的通道数</span></span><br><span class="line">num_channels, growth_rate = <span class="number">64</span>, <span class="number">32</span></span><br><span class="line">num_convs_in_dense_blocks = [<span class="number">4</span>, <span class="number">4</span>, <span class="number">4</span>, <span class="number">4</span>]</span><br><span class="line">blks = []</span><br><span class="line"><span class="keyword">for</span> i, num_convs <span class="keyword">in</span> <span class="built_in">enumerate</span>(num_convs_in_dense_blocks):</span><br><span class="line">    blks.append(DenseBlock(num_convs, num_channels, growth_rate))</span><br><span class="line">    <span class="comment"># 上一个稠密块的输出通道数</span></span><br><span class="line">    num_channels += num_convs * growth_rate</span><br><span class="line">    <span class="comment"># 在稠密块之间添加一个转换层，使通道数量减半</span></span><br><span class="line">    <span class="keyword">if</span> i != <span class="built_in">len</span>(num_convs_in_dense_blocks) - <span class="number">1</span>:</span><br><span class="line">        blks.append(transition_block(num_channels, num_channels // <span class="number">2</span>))</span><br><span class="line">        num_channels = num_channels // <span class="number">2</span> <span class="comment">#//2, 表示除法，向下取整数</span></span><br><span class="line">        </span><br><span class="line">net = nn.Sequential(</span><br><span class="line">    b1, *blks,</span><br><span class="line">    nn.BatchNorm2d(num_channels), nn.ReLU(),</span><br><span class="line">    nn.AdaptiveAvgPool2d((<span class="number">1</span>, <span class="number">1</span>)),</span><br><span class="line">    nn.Flatten(),</span><br><span class="line">    nn.Linear(num_channels, <span class="number">10</span>))</span><br></pre></td></tr></table></figure><h2 id="RNN-循环卷积"><a href="#RNN-循环卷积" class="headerlink" title="RNN 循环卷积"></a>RNN 循环卷积</h2><h3 id="序列模型"><a href="#序列模型" class="headerlink" title="序列模型"></a>序列模型</h3><p>预测明天的股价要比过去的股价更困难，尽管两者都只是估计一个数字。 毕竟，先见之明比事后诸葛亮难得多。 在统计学中，前者（对超出已知观测范围进行预测）称为_<strong>外推法</strong><em>（extrapolation）， 而后者（在现有观测值之间进行估计）称为</em><strong>内插法_</strong>（interpolation）</p><p><mark style="background:#fff88f">音乐、语音、文本和视频都是连续的</mark>， 而且这个顺序是有意义的。</p><p>处理<strong>序列数据</strong>需要<strong>统计工具</strong>和<strong>新的深度神经网络架构</strong></p><p>前面我们的FNN，CNN，都是以单个样本为单位（X，y）来作为独立的计算单元，不同的样本之间相互独立。目标是，给定一个新的X，我们能够产生对他的标签的预测y’</p><p>但是现在，我们开始讨论<strong>序列模型</strong>：输入X，不是单独的一个样本，而是和时间步有关</p><p>我们的目的是,通过历史的输入序列（xt-1, xt-2, …., x1）, 预测下一个时间步xt<br><img src="/images/Pasted%20image%2020260503155431.png" alt="251"></p><p>我们现在把研究的中心，放到对一个序列的预测上</p><p><img src="/images/30cc6650562dca8dba76dccc480bac21.jpg" alt="578"></p><p>主要有下面几种<strong>统计工具</strong>：</p><h4 id="自回归模型"><a href="#自回归模型" class="headerlink" title="自回归模型"></a>自回归模型</h4><p>为了实现依据{xt-1, …. , x1}的历史序列，对xt的预测，我们可以使用回归模型。就像之前我们的线性回归一样（因为想要预测数字嘛）</p><p>但是这会有一个问题：输入数据{xt-1, …., x1}这个数据，本身就是和t有关的，</p><blockquote><p>这个从t&#x3D;1时刻开始，一直到我们要求t的前一时刻，一共t-1个数据点&#x2F;时间步，这个数据量，本身就是随着t的增大而增大的。<br>预测t&#x3D;10的数字，我们的输入是9个时间步， 预测t&#x3D;10000的数字，我们的输入是9999个时间步</p></blockquote><p>所以，我们需要一个<strong>近似方法</strong>来让这个计算变得容易处理</p><p>因为我们的理论目标是要估计P(xt | xt-1, …., x1), 但是代价是依赖的输入数据随着t的增大而代价变得越来越大。</p><p>所以我们的<mark style="background:#fff88f">近似方法</mark>，就是找一个方法<code>P&#39; ≈ P(xt | xt-1, ...., x1)</code></p><p>有这样<mark style="background:#affad1">两种策略</mark>：</p><ul><li><strong>第一种策略</strong>：假设现实中，不需要从最初的时间步t&#x3D;1开始，仅需要满足某个长度r的序列长度即可，即使用<br><code>P(xt | xt-1, xt-2, ...., xt-r) ≈ P(xt | xt-1, ...., x1)</code>, 好处是，总的输入参数不变，至少在t&gt;r时是这样子的（因为t&lt;r时，还没有r个时间步呢）<ul><li>这样我们就能够训练一个上面提到的深度网络：这种模型被称为<mark style="background:#fff88f">自回归模型</mark>， 因为他是对自己回归的。（自己预测出来的输出，又反过来加入了输入，再次输入到模型，自产自销）</li></ul></li><li><strong>第二种策略</strong>：保留一些对过去观测的总结ht（状态），并且同时产生输出xt_hat, 和状态ht, 此时的输出就是用：<code>xt_hat = P(xt | ht)</code> 估计xt（<strong>输出方程</strong>）, ht &#x3D; g(ht-1, xt-1) 作为<strong>状态转移</strong>。<ul><li>因为这种模型，内部的ht是无法被观测的，所以被叫做<mark style="background:#fff88f">隐变量自回归模型</mark></li></ul></li></ul><p><img src="/images/7792ff16418ebc10db00ea92d775aa59.jpg" alt="472"><br><strong>纠正一下时间步的对应关系</strong><br><img src="/images/31fff10de21302e259b255a796208e0f.jpg"></p><p>那如何来生成训练数据？</p><blockquote><p>因为我们不能指望依赖静止的数据，预测新的变化，因为新的动力学本身一定收到新的数据的影响</p></blockquote><p>所以，整个序列的估计值，可以通过以下方式获取：</p><p><img src="/images/Pasted%20image%2020260504101838.png" alt="308"></p><p>简单说就是，这个公式描述了，<strong>如何计算一个长度为 $T$ 的观测序列 $(x_1, \dots, x_T)$ 出现的联合概率</strong></p><p>这里我觉得要分清：</p><ul><li>观测：x, 是真实发生的一个序列</li><li>预测：x_hat, 也就是对观测的预测&#x2F;估计，也就是P(x)<br><img src="/images/6c687ff551c6f7a4b9d4a4aefb6beef3.jpg" alt="423"></li></ul><h5 id="模型和概率模型的理解"><a href="#模型和概率模型的理解" class="headerlink" title="模型和概率模型的理解"></a><mark style="background:#ff4d4f">模型和概率模型的理解</mark></h5><p>我的理解是，我们深度学习，建立的模型，来对输出进行预测，本质上，是得到一个输出的分布，也就是概率模型</p><p>所以，这个式子，左边里面的x是实际发送的序列，是<strong>观测</strong>，而我们的自回归这些序列模型，是期望得到<strong>观测的概率</strong>，也就是预测，所以我们的序列模型要实现的是一个概率模型P(x)，他得到的是预测（对观测的一个概率分布）</p><p>触及了<strong>概率图模型</strong>和深度学习<strong>序列建模</strong>的核心本质</p><ol><li>观测与概率的关系</li></ol><ul><li><strong>左边的 $P(x_1, \dots, x_T)$</strong>：确实代表了整个<strong>实际观测序列</strong>在真实世界中发生的概率分布。</li><li><strong>模型的任务</strong>：我们的自回归模型（如 TCN、LSTM）本质上就是在<mark style="background:#fff88f">尝试拟合（Approximating）这个真实分布</mark>。我们希望模型学到的分布 $P_{model}$ 尽可能接近真实观测的分布 $P_{real}$。</li></ul><ol><li>预测即概率分布</li></ol><ul><li><strong>预测的本质</strong>：正如你所说，<mark style="background:#fff88f">序列模型</mark>实现的确实是<mark style="background:#ff4d4f">一个概率模型</mark>。当我们说“预测下一个值”时，模型输出的其实不是一个单一的绝对数值，而是在给定历史条件下，下一个可能观测值的<strong>概率分布</strong>。</li><li><strong>回归 vs 分类</strong>：<ul><li>在离散场景（如 NLP）中，它是所有单词的概率表。</li><li>在连续场景（如你研究的<strong>外骨骼扭矩预测</strong>）中，模型虽然通常输出一个具体数值 $\hat{x}_t$，但从数学上看，这相当于假设概率分布是一个以 $\hat{x}_t$ 为均值的高斯分布。</li></ul></li></ul><p>所以</p><p><strong>链式法则的意义</strong>：公式 (8.1.2) 将复杂的全局概率 $P(x_1, \dots, x_T)$ 拆解为一个个可计算的局部步步预测 $P(x_t \mid x_{t-1}, \dots, x_1)$。</p><p><strong>期望得到预测</strong>：通过最大化观测序列在模型下的出现概率（即最大似然估计 MLE），我们强迫模型在给定历史 $(x_{t-1}, \dots, x_1)$ 时，能给真实的 $x_t$ 分配最高的概率。这样，模型就具备了“预测”能力</p><p>两个问题：<br><mark style="background:#affad1">既然我们的序列模型，是为了学习数据的概率分布规律，是这么理解吧，但是模型最终会输出一个具体的预测值啊，并不是输出完整的分布规律比如期望，均值这些。那他输出的内容和这个随机变量属于的分布律是什么关系呢</mark></p><p>虽然模型只给你一个数值，但这个数值背后隐藏着一整个分布律：</p><ul><li><strong>点估计（Point Estimate）</strong>：你目前看到的输出是一个点估计。它只是分布律中最具有代表性的一个点（通常是均值或众数）。</li><li><strong>隐式分布</strong>：模型其实学到了整个 $P(x_t \mid x_{t-1}, \dots, x_1)$，但在最后的输出层（Output Layer），它为了给你一个确定的执行指令（比如给外骨骼电机一个确定的电流值），它不得不把整个分布“压缩”成了这一个点。</li></ul><p><img src="/images/Pasted%20image%2020260504104137.png" alt="594"></p><p>那么又带来一个问题：<mark style="background:#affad1">是怎么实现让回归模型，分类模型，输出的值，是选择的条件概率分布的期望值，还是概率最大的那一个？</mark></p><p><strong>损失函数定义了“惩罚”规则，而模型为了躲避惩罚，被迫学会了输出该分布下的特定统计量。</strong></p><p>对于回归模型来说，为什么输出是期望值（均值）</p><p><mark style="background:#fff88f">1. 回归模型：为什么输出的是“期望值（均值）”？</mark></p><p>当你训练 TCN 预测外骨骼扭矩并使用 <strong>L2 Loss（均方误差 MSE）</strong> 时：</p><ul><li><strong>惩罚机制</strong>：MSE 的计算方式是 $Loss &#x3D; (y - \hat{y})^2$。</li><li><strong>数学逼近</strong>：从概率统计的角度看，使 MSE 最小化的最优预测值 $\hat{y}$ 正好就是条件概率分布的<strong>均值（期望）</strong>。<ul><li><mark style="background:#b1ffff"><em>原理</em>：如果你有一个分布，你想找一个点，让所有样本到这个点的距离平方和最小，这个点在数学上证明必然是均值</mark>。</li></ul></li><li><strong>例外情况</strong>：如果你改用 <strong>L1 Loss（平均绝对误差 MAE）</strong>，即 $Loss &#x3D; |y - \hat{y}|$，模型为了最小化损失，会学会输出分布的<strong>中位数（Median）</strong>。</li></ul><p><strong>结论</strong>：通过使用 MSE 损失函数，你就在数学上“命令”模型去寻找并输出分布的期望值。</p><p>对于分类模型来说，为什么输出的是概率最大的值</p><p><mark style="background:#fff88f">2. 分类模型：为什么输出的是“概率最大的那一个”？</mark><br>当你做动作识别并使用 <strong>交叉熵损失（Cross-Entropy Loss）</strong> 时：</p><ul><li><strong>惩罚机制</strong>：交叉熵会衡量模型输出的概率分布与真实标签（One-hot 编码）之间的差异。</li><li><strong>数学逼近</strong>：交叉熵的本质是最大似然估计（MLE）。它会迫使模型在正确的类别上分配尽可能高的概率。</li><li><strong>推理决策</strong>：在推理阶段，虽然 Softmax 层给出了所有类别的概率分布，但我们通常通过 <code>argmax()</code> 操作提取概率最高的索引。</li><li><strong>数学本质</strong>：这对应于离散分布中的<strong>众数（Mode）</strong>。模型被训练成给最可能发生的事件分配最高权重。</li></ul><hr><p>所以，假设我们面对回归问题，选取不同的损失函数，实际上，是影响模型输出取的分布的估计量是吗</p><p>所以我总结以下就是：</p><p>我们在回归模型里面，我们的模型学会的是条件概率分布，他会让输出满足一个条件概率分布，（条件就是输入），所以我们的模型就是一个条件概率模型。</p><p>训练数据集，就是实际蕴含分布律规律的采样点。</p><p>损失函数，其实就是关于我们如何来从这个模型的条件概率分布率，给定条件的分布率下，得到一个具体的值，也就是该条件概率分布下的估计量</p><p><mark style="background:#affad1">gemini的答案：</mark></p><ol><li><strong>模型</strong>即<strong>条件概率模型</strong>：$P(y|x)$<br>在回归问题中，给定输入 $x$（如你的 IMU 传感器数据），模型不再只是一个简单的函数 $f(x)$，它本质上是在建模 <strong>$P(y|x)$</strong></li></ol><p><strong>输入 $x$ 是“条件”</strong>：它限定了当前系统所处的上下文（Context）</p><p><strong>输出满足分布</strong>：对于同一个输入 $x$，真实世界中的 $y$（扭矩）可能因为噪声或未观测变量（如肌肉疲劳）而存在波动，这个波动形成的规律就是条件概率分布</p><p><strong>模型是“参数化分布”</strong>：你的 TCN 网络其实就是这个概率模型的参数化表达，它学习如何根据不同的 $x$ 去调整分布的形态。</p><ol start="2"><li><strong>训练集即“样本采样”</strong></li></ol><ul><li><strong>数据集是观测点</strong>：每一组 $(x, y)$ 都是从那个未知的、真实的联合分布 $P(x, y)$ 中进行的 一次<strong>随机采样</strong>。</li><li><strong>训练的目标</strong>：我们手头只有离散的采样点，但我们想通过这些点去反推（拟合）出那个连续的条件概率分布 $P(y|x)$。</li></ul><ol start="2"><li>损失函数即“估计量提取器”<br>这是最核心的一点。既然模型学到了一个完整的分布，但它最后只能吐出一个具体的标量值，<strong>损失函数就决定了这个值的“身份”</strong>：</li></ol><ul><li><strong>MSE (均方误差)</strong>：强制模型输出该分布的<strong>期望（均值）</strong>。这在物理上代表了“最小能量”或“平均功率”意义上的最优估计。</li><li><strong>MAE (平均绝对误差)</strong>：强制模型输出该分布的<strong>中位数</strong>。这在处理有大幅噪声（离群值）的外骨骼数据时非常有用，因为它不会被偶尔的传感器脉冲带偏。</li><li><strong>分位数损失 (Quantile Loss)</strong>：如果你想知道扭矩有 90% 的概率不会超过多少，你可以通过这个损失函数让模型输出分布的 <strong>90% 分位数</strong>。</li></ul><p><img src="/images/2915bf6e766fa21b228e621708f64c2a.jpg"></p><hr><p>所以，<mark style="background:#affad1">我想再总结以下，</mark></p><blockquote><p>我目前从最开始的线性回归单元，到MLP，到MLP+softmax, 到CNN卷积，到现在的RNN序列模型</p><p>除了带softmax的那个是分类模型，其他都是回归模型是吧。</p><p>比如MLP，回归，学习的是（X ,Y ）的分布律，然后MLP（x）从联合分布律中得到条件分布率，然后指定使用的损失函数MSE训练，来设置模型的输出是选择的条件分布率的均值估计值。</p><p>又比如MLP+softmax分类，学习的是（X，Y）的分布律，也是得到条件分布律，然后用交叉熵损失函数训练，设置模型选择条件分布律中的概率最大的值。</p><p>又比如CNN+全连接的LeNet 这些，还是回归模型？还是分类模型？还是和上面一样的理解？</p><p>现在的RNN序列模型，本质上，是学习{(xt-1, xt-2, …., x1), xt(标签真值)}序列的概率分布，然后模型得到输入（xt-1, xt-1, …., x1）后，就得到了p(xt | xt-1, xt-2, …, x1)的条件概率分布了。之后损失函数让我们继续从这个条件分布里面得到一个具体的估计量。也就是xt_hat是吗</p></blockquote><p>我们可以顺着你的思路，把这些模型一一对号入座，完善你的理论版图：</p><p><mark style="background:#fff88f"> 1. 回归与分类的界限</mark></p><p>你的理解非常准确。<strong>回归与分类的本质区别不在于中间的卷积或循环层，而在于最后的“输出层”和“损失函数”：</strong></p><ul><li><strong>线性回归、MLP、CNN（用于预测数值）</strong>：只要末端输出的是连续值且配合 MSE&#x2F;MAE 等损失函数，它们都是<strong>回归模型</strong>。</li><li><strong>MLP + Softmax、CNN + Softmax (如 LeNet)</strong>：只要末端使用了 Softmax 将输出映射为概率分布，并配合交叉熵损失，它们就是<strong>分类模型</strong>。</li></ul><p><mark style="background:#fff88f"> 2. 对 MLP 和分类模型的复盘</mark></p><ul><li><strong>MLP 回归</strong>：完全如你所说，它建模 $P(Y|X)$，通过 MSE 引导模型输出条件分布的<strong>均值</strong>。</li><li><strong>MLP + Softmax 分类</strong>：它建模离散的条件分布 $P(Y|X)$。交叉熵训练使得模型在正确类别上的概率最大化，最终推理时我们取其<strong>众数（概率最大者）</strong>。</li></ul><p><mark style="background:#fff88f"> 3. 如何理解 CNN (如 LeNet)？</mark></p><p><strong>LeNet 既可以是分类模型，也可以是回归模型，取决于你拿它做什么：</strong></p><ul><li><strong>做分类（原始用途）</strong>：LeNet 最后一层接 Softmax 识别数字 0-9。此时它学习的是 $P(数字类别 | 图像像素)$。它依然是得到条件分布，然后选概率最大的。</li><li><strong>做回归</strong>：如果你把 LeNet 最后的 Softmax 去掉，让它预测图像中物体的坐标（连续值），它就变成了回归模型。逻辑瞬间切换到：得到 $P(坐标 | 图像)$ 的条件分布，通过 MSE 输出<strong>均值估计</strong>。</li><li><strong>本质逻辑</strong>：CNN 的卷积层只是在做<strong>特征提取</strong>（把高维像素空间映射到低维特征空间），而最后的“全连接层 + 损失函数”才决定了它是如何从条件分布中提取估计量的。</li></ul><p><mark style="background:#fff88f">4. 对 RNN&#x2F;TCN 序列模型的升华</mark></p><p>你对 RNN 的总结非常精辟，这就是自回归（Autoregressive）的精髓：</p><ul><li><strong>学习目标</strong>：它学习的是序列的联合概率分布 $P(x_1, \dots, x_T)$。</li><li><strong>条件化</strong>：通过链式法则，模型在每一个步长 $t$，都在计算 $P(x_t \mid x_{t-1}, \dots, x_1)$。</li><li><strong>估计量提取</strong>：<ul><li><strong>输入</strong>：历史序列 $(x_{t-1}, \dots, x_1)$。</li><li><strong>得到</strong>：当前时刻的条件分布。</li><li><strong>输出 $\hat{x}_t$</strong>：损失函数（如你在 RTX 4090 上用的 MSE）决定了 $\hat{x}_t$ 是这个分布的<strong>均值（期望）</strong>。</li></ul></li></ul><p><img src="/images/Pasted%20image%2020260504114045.png"></p><p><strong>结论：</strong></p><p>是的，你的理解完全正确！无论模型多复杂，它都在做两件事：</p><ol><li><p><strong>建模条件分布 $P(Y \mid X)$</strong>。</p></li><li><p><strong>根据损失函数从分布中“抠”出一个最代表性的值作为输出</strong>。</p></li></ol><hr><p>最后一个问题</p><blockquote><p>那么，还有最后一个问题，对于序列模型，P(x1, x2, ……, xT),这个该叫什么？为什么是要预测一整个序列的概率？</p><p>我们普通的回归模型，给定x后，模型得到P(Y|X)的条件概率分布，然后选出一个估计量，作为y的具体值。</p><p>但是为什么序列模型，我能理解模型每次推理得到的是P(xt | xt-1, ….., x1), 把这些每次的概率乘起来，是不是就相当于自回归？</p><p>还是说，如果我是离线测试，我已经拥有了完整的序列，这个时候还叫自回归嘛？我并没有把自己预测的xt，当作下一次推理的输入，而是把原本数据集里面的xt当作输入，这种离线测试。</p><p>还是说自回归，比如要把自己的预测值，重新作为输入？</p></blockquote><p><mark style="background:#affad1"> 1. P(x1, x2, …, xT) 叫什么？</mark></p><p>在统计学中，这被称为<strong>联合概率分布（Joint Probability Distribution）</strong>。</p><ul><li><p><strong>为什么要预测一整个序列的概率？</strong> 因为序列中的数据不是独立的，前后的数值存在强耦合（动力学规律）。预测整个序列的概率，本质上是在评估“这一串数据作为一个整体，符合真实物理规律的可能性有多大”。</p></li><li><p><strong>整体与局部的关系</strong>：</p><p>  根据链式法则，整个序列的概率被分解为每一步条件概率的乘积：</p><p>  $$P(x_1, \dots, x_T) &#x3D; \prod_{t&#x3D;1}^T P(x_t \mid x_{t-1}, \dots, x_1)$$</p><p>  。所以，虽然我们每次只算一步 $P(x_t \mid \dots)$，但我们的终极目标是让整条曲线（联合分布）最接近真实情况。</p></li></ul><p><mark style="background:#affad1">2. 什么是真正的“自回归”？</mark></p><p>你的直觉非常敏锐。在工程实践中，根据你如何处理“预测值”，会演变成两种完全不同的模式：</p><p> <mark style="background:#fff88f">模式 A：Teacher Forcing（你说的“离线测试&#x2F;训练”）</mark></p><p>在这种模式下，即使模型在 $t$ 时刻预测错了，你在 $t+1$ 时刻依然把数据集里的真值（Ground Truth）喂给模型。</p><ul><li><p><strong>是否叫自回归？</strong> 严格来说，这叫<strong>自回归模型的训练&#x2F;并行推理</strong>。它利用了序列的自回归结构，但没有形成反馈环。</p></li><li><p><strong>特点</strong>：计算速度快（可以并行），且不会产生误差累积。你目前在 RTX 4090 上训练 TCN 预测扭矩，大概率就是这种模式。</p></li></ul><p><mark style="background:#fff88f"> 模式 B：Autoregressive Generation（真正的“闭环自回归”）</mark></p><p>当你真正要把预测出的 $\hat{x}<em>t$ <strong>重新当作输入</strong>喂回给模型，去预测 $\hat{x}</em>{t+1}$ 时，这才是最纯正的自回归生成。</p><ul><li><p><strong>特点</strong>：这是一种迭代过程。模型必须对自己产生的错误负责，误差会随着时间步不断累积（Drift）。</p></li><li><p><strong>应用场景</strong>：比如 ChatGPT 生成文本，或者外骨骼在没有任何传感器输入的情况下，只靠自己预测下一秒的步态趋势。</p></li></ul><p><img src="/images/Pasted%20image%2020260504144736.png" alt="563"></p><h4 id="马尔可夫模型"><a href="#马尔可夫模型" class="headerlink" title="马尔可夫模型"></a>马尔可夫模型</h4><p>我们之前在自回归模型的近似法中，用r序列长度的序列，来代替完整的历史序列{xt-1, …., x1}</p><p>只要这种是近似精确的，我们就说序列<strong>满足马尔可夫条件</strong></p><p>特别的，如果r等于1，就是一阶马尔可夫模型，也就是条件仅依赖上一个时间步的输入，此时公式如下：</p><p><img src="/images/Pasted%20image%2020260504144958.png" alt="462"></p><p>比如说这个，我们就可以使用<strong>条件概率公式</strong>+<strong>全概率公式</strong><br><img src="/images/Pasted%20image%2020260504145145.png" alt="610"><br><img src="/images/Pasted%20image%2020260504145300.png"><br>这个是隐马尔可夫的动态规划问题，后面讲。这种动态规划在控制算法和强化学习中用的多一点</p><h4 id="因果关系"><a href="#因果关系" class="headerlink" title="因果关系"></a>因果关系</h4><p>原则上P(x1, x2, …, xt) 倒序展开也没啥问题，基于条件概率公式，总是能写出顺序的概率推导，因为乘法具有交换律<br><img src="/images/Pasted%20image%2020260504145518.png" alt="320"></p><p>下面我们<mark style="background:#fff88f">自己实现一个自回归模型</mark>，在这之前，</p><p><img src="/images/Pasted%20image%2020260504204208.png" alt="491"><br><img src="/images/Pasted%20image%2020260504204225.png" alt="487"></p><p><img src="/images/Pasted%20image%2020260504204242.png" alt="543"></p><p>自回归（Autoregressive）从任务属性上分类，完全属于回归（Regression）模型。</p><ul><li><p><strong>目标一致</strong>：普通回归预测的是 $P(Y|X)$，而自回归预测的是 $P(x_t | x_{t-1}, \dots, x_{t-\tau})$。由于输出 $x_t$ 是连续的数值（你的正弦波数据），所以它遵循回归任务的所有统计规律。</p></li><li><p><strong>损失函数验证</strong>：你使用的 <code>nn.MSELoss</code>（均方误差）正是回归任务的标准配置。如我们之前讨论的，它强迫 MLP 输出条件分布的<strong>期望值</strong>。</p></li></ul><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"><span class="keyword">import</span> matplotlib.pyplot <span class="keyword">as</span> plt</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">先构造1000个时间步的数据，x = 正弦+噪声</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">T = <span class="number">1000</span></span><br><span class="line">time = torch.arange(<span class="number">1</span>, T+<span class="number">1</span>, dtype=torch.float32)</span><br><span class="line">x = torch.sin(<span class="number">0.01</span> * time) + torch.normal(<span class="number">0</span>, <span class="number">0.2</span>, (T,))</span><br><span class="line"></span><br><span class="line"><span class="comment"># x = [0号， 1号，2号，......，  999号]  序列值</span></span><br><span class="line"><span class="comment"># time = [0, 1, 2, ..........,  999 ]   时间步</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">准备数据集：</span></span><br><span class="line"><span class="string">一个长度为4的序列为特征，下一个值为标签， 用前600个时间步来训练</span></span><br><span class="line"><span class="string">(xt-4, xt-3, xt-2, xt-1)~(xt)</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">tau = <span class="number">4</span> <span class="comment"># 序列长度=4</span></span><br><span class="line">features = torch.zeros((T-tau, tau))</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(tau):</span><br><span class="line">    features[:,i] = x[i:T-tau+i]</span><br><span class="line">labels = x[tau:].reshape((-<span class="number">1</span>,<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line">batch_size=<span class="number">16</span></span><br><span class="line">n_train = <span class="number">600</span></span><br><span class="line">  </span><br><span class="line"><span class="string">&quot;&quot;&quot;取:n_train, 即前600行的（feature,labels）作为训练集， 构造训练集迭代器&quot;&quot;&quot;</span></span><br><span class="line">train_iter = d2l.load_array((features[:n_train], labels[:n_train]),</span><br><span class="line">                            batch_size, is_train=<span class="literal">True</span>)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">数据集如下</span></span><br><span class="line"><span class="string">features = [0号,1号,2号,3号]    labels = [4号]</span></span><br><span class="line"><span class="string">           [1号,2号,3号,4号]             [5号]</span></span><br><span class="line"><span class="string">            ...                          ...</span></span><br><span class="line"><span class="string">           [995号,996号,997号,998号]     [999号]</span></span><br><span class="line"><span class="string">           </span></span><br><span class="line"><span class="string">features.shape = (996,4)        labels.shape = (996,1)</span></span><br><span class="line"><span class="string">训练集，从（features, labels）里面选取前600行，batch_size = 16</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#用xavier初始化全连接层的权重矩阵</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_weights</span>(<span class="params">m</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">        nn.init.xavier_normal_(m.weight)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">简单的构造一个由全连接构造的回归模型</span></span><br><span class="line"><span class="string">所以，这个模型仅仅只是实现一个自回归的模型，（RNN是循环卷积，本质上是自回归模型的子类）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_net</span>():</span><br><span class="line">    net = nn.Sequential(nn.Linear(<span class="number">4</span>,<span class="number">10</span>),</span><br><span class="line">                        nn.ReLU(),</span><br><span class="line">                        nn.Linear(<span class="number">10</span>,<span class="number">1</span>))</span><br><span class="line">    net.apply(init_weights)</span><br><span class="line">    <span class="keyword">return</span> net</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;定义损失函数对象，因为是回归模型，预测序列, 返回每个序列样本的损失张量，不计算均值&quot;&quot;&quot;</span></span><br><span class="line">loss = nn.MSELoss(reduction = <span class="string">&#x27;none&#x27;</span>)</span><br><span class="line">  </span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;下面就开始准备训练模型了&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">train</span>(<span class="params">net, train_iter, loss, epochs, lr</span>):</span><br><span class="line">    trainer = torch.optim.Adam(net.parameters(), lr)</span><br><span class="line">    <span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(epochs):</span><br><span class="line">        <span class="keyword">for</span> X,Y  <span class="keyword">in</span> train_iter:</span><br><span class="line">            trainer.zero_grad()</span><br><span class="line">            l = loss(net(X),Y)</span><br><span class="line">            l.<span class="built_in">sum</span>().backward()</span><br><span class="line">            trainer.step()</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;epoch %d, loss %4f&quot;</span> % (epoch, d2l.evaluate_loss(net, train_iter, loss)))</span><br><span class="line"></span><br><span class="line">  </span><br><span class="line"></span><br><span class="line">net = get_net()</span><br><span class="line">net.train()</span><br><span class="line">train(net, train_iter, loss, <span class="number">5</span>, <span class="number">0.01</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">  </span><br><span class="line">net.<span class="built_in">eval</span>()</span><br><span class="line">onestep_preds = net(features)</span><br><span class="line">d2l.plot([time, time[tau:]], <span class="comment"># 横轴显示两个量：time(完整1000个数据)[0,1000), 测试用[4,1000)</span></span><br><span class="line">         <span class="comment"># 纵轴显示两个量：x(完整的1000个数据值)[0,1000), 测试用的预测值</span></span><br><span class="line">         [x.detach().numpy(), onestep_preds.detach().numpy()],</span><br><span class="line">         <span class="string">&#x27;time&#x27;</span>, <span class="string">&#x27;x&#x27;</span>,</span><br><span class="line">         legend=[<span class="string">&#x27;data&#x27;</span>, <span class="string">&#x27;1-step preds&#x27;</span>],</span><br><span class="line">         xlim=[<span class="number">1</span>, <span class="number">1000</span>],</span><br><span class="line">         figsize=(<span class="number">6</span>, <span class="number">3</span>))</span><br><span class="line"></span><br><span class="line">plt.savefig(<span class="string">&quot;prediction_plot.png&quot;</span>, dpi=<span class="number">300</span>, bbox_inches=<span class="string">&quot;tight&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可选：关闭图，释放内存</span></span><br><span class="line"></span><br><span class="line">plt.close()</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260504204303.png" alt="394"></p><p>由此可见，<mark style="background:#affad1">单步预测</mark>效果都不错（<code>(xt-4, xt-3, xt-2, xt-1)~(xt)</code>）</p><p>但此时针对这600个训练样本，<br><img src="/images/Pasted%20image%2020260504205347.png" alt="495"></p><p>这里还有一个问题，当我们的标签数据没有了之后（观察序列只到604）<br><img src="/images/Pasted%20image%2020260504205540.png" alt="552"></p><p>所以，<mark style="background:#fff88f">K步预测，本质上就是执行k次单步预测</mark>， 所以这里的k步，是说，<strong>一次性预测多少个未来的数据</strong></p><p><mark style="background:#affad1">k步预测</mark><br><img src="/images/Pasted%20image%2020260504204512.png"></p><p><strong>“k 步预测”指的就是在当前时刻，仅凭现有的观测数据，连续向未来推算 $k$ 个时间点的数值</strong></p><p><img src="/images/Pasted%20image%2020260504204438.png" alt="269"></p><blockquote><p>所以这里要把K步预测，和你给到多少真实标签，和你实际有多少真值标签分开来<br>就是说，现在输入，给到604的真实标签，这个时候，你去预测未来的k步，你不准用605向后的真实标签，所以就出现上图所示，自己用自己的预测值作为输入。<strong>本质上还是离线里面的局部在线</strong></p></blockquote><p>所以，下面我们来做一个<strong>k步预测，把k弄大一点</strong>，比如到604步之后，就不提供标签真值作为输入了，让他预测400步的预测。也就是<strong>400步预测</strong>，k&#x3D;400<br><img src="/images/Pasted%20image%2020260504212535.png" alt="487"></p><p>可以看到，<strong>这个k很大之后，这个预测就不好了</strong>，我们要进行400步的预测，自己左脚踩右脚，结果发现，预测的结果，<strong>很快衰减到一个常数</strong>。</p><p><strong>事实是由于错误的累积</strong></p><p><img src="/images/Pasted%20image%2020260504212711.png"></p><p><img src="/images/Pasted%20image%2020260504212722.png"></p><p>以上例子清楚地说明了<strong>当我们试图预测更远的未来时</strong>，<strong>预测的质量是如何变化的</strong>（越来越差）。 虽然“步预测”看起来仍然不错，但超过这个跨度的任何预测几乎都是无用的。</p><h3 id="文本预处理"><a href="#文本预处理" class="headerlink" title="文本预处理"></a>文本预处理</h3><p>这一节主要针对文本来考虑如何构造数据集</p><ul><li>从磁盘加载文本文件到内存成字符串</li><li>字符串拆分成token</li><li>token的表</li><li>token转成张量</li></ul><p><img src="/images/ae19c51525de1999fdc89dfcfcff76b3.jpg" alt="531"></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">d2l.DATA_HUB[<span class="string">&#x27;time_machine&#x27;</span>] = (d2l.DATA_URL + <span class="string">&#x27;timemachine.txt&#x27;</span>, <span class="string">&#x27;090b5e7e70c295757f55df93cb0a180b9691891a&#x27;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">read_time_machine</span>():</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(d2l.download(<span class="string">&#x27;time_machine&#x27;</span>), <span class="string">&#x27;r&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">        lines = f.readlines()</span><br><span class="line">    <span class="keyword">return</span> [re.sub(<span class="string">&#x27;[^A-Za-z]+&#x27;</span>, <span class="string">&#x27; &#x27;</span>, line).strip().lower() <span class="keyword">for</span> line <span class="keyword">in</span> lines]</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;把文本数据加载成一个动态数组lines&quot;&quot;&quot;</span></span><br><span class="line">lines = read_time_machine()</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(lines[<span class="number">0</span>]) <span class="comment"># the time machine by h g wells</span></span><br><span class="line"><span class="built_in">print</span>(lines[<span class="number">14</span>]) <span class="comment"># caressed us rather than submitted to be sat upon and there was that</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;token化&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">tokenize</span>(<span class="params">lines, token=<span class="string">&#x27;word&#x27;</span></span>):</span><br><span class="line">    <span class="keyword">if</span> token== <span class="string">&#x27;word&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> [line.split() <span class="keyword">for</span> line <span class="keyword">in</span> lines]</span><br><span class="line">    <span class="keyword">elif</span> token == <span class="string">&#x27;char&#x27;</span>:</span><br><span class="line">        <span class="keyword">return</span> [<span class="built_in">list</span>(line) <span class="keyword">for</span> line <span class="keyword">in</span> lines]</span><br><span class="line">    <span class="keyword">else</span>: </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;error+&quot;</span>, token)</span><br><span class="line"></span><br><span class="line">tokens = tokenize(lines)</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">11</span>):</span><br><span class="line">    <span class="built_in">print</span>(tokens[i])</span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;构造词表</span></span><br><span class="line"><span class="string">把token lists的每一个token进行编码，映射到从0开始的数字索引</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">count_corpus</span>(<span class="params">tokens</span>):  <span class="comment">#@save</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;统计词元的频率&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 这里的tokens是1D列表或2D列表</span></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(tokens) == <span class="number">0</span> <span class="keyword">or</span> <span class="built_in">isinstance</span>(tokens[<span class="number">0</span>], <span class="built_in">list</span>):</span><br><span class="line">        <span class="comment"># 将词元列表展平成一个列表</span></span><br><span class="line">        tokens = [token <span class="keyword">for</span> line <span class="keyword">in</span> tokens <span class="keyword">for</span> token <span class="keyword">in</span> line]</span><br><span class="line">    <span class="keyword">return</span> collections.Counter(tokens)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Vocab</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, tokens=<span class="literal">None</span>, min_freq=<span class="number">0</span>, reserved_tokens=<span class="literal">None</span></span>):</span><br><span class="line">        <span class="keyword">if</span> tokens <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            tokens = []</span><br><span class="line">        <span class="keyword">if</span> reserved_tokens <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            reserved_tokens = []</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 统计出现频率</span></span><br><span class="line">        counter = count_corpus(tokens)</span><br><span class="line">        <span class="comment"># counter.items()转换格式，key=lambda指定规则，reserve指定降序排序</span></span><br><span class="line">        <span class="variable language_">self</span>._token_freqs = <span class="built_in">sorted</span>(counter.items(), key=<span class="keyword">lambda</span> x:x[<span class="number">1</span>], reverse=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 构造词包</span></span><br><span class="line">        <span class="comment"># idx_to_token为动态数组，用下标来作为token的身份号ID</span></span><br><span class="line">        <span class="comment"># 先将第一个&lt;unk&gt; 未知，放在第一个ID=0， 如果有预留的token，就是顺后添加</span></span><br><span class="line">        <span class="variable language_">self</span>.idx_to_token = [<span class="string">&#x27;&lt;unk&gt;&#x27;</span>] + reserved_tokens</span><br><span class="line">        <span class="comment"># token_to_idx为字典(哈希表)，添加（token:ID）</span></span><br><span class="line">        <span class="comment"># 先将idx_to_token里面获得ID号的token对加入字典</span></span><br><span class="line">        <span class="variable language_">self</span>.token_to_idx = &#123;token: idx</span><br><span class="line">                             <span class="keyword">for</span> idx, token <span class="keyword">in</span> <span class="built_in">enumerate</span>(<span class="variable language_">self</span>.idx_to_token)&#125;</span><br><span class="line">        </span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        开始对我们的数据集里面所有的token，按照出现频率，添加到idx_to_token获得ID号，然后把这个对</span></span><br><span class="line"><span class="string">        添加到token_to_idx里面。通过len(self.idx_to_token)-1来获得ID是多少，可以避免遍历</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">        reserved_tokens不出现在原始语料库里面</span></span><br><span class="line"><span class="string">        &lt;pad&gt; 填充符，保证输入的序列（一行文本）长度一致</span></span><br><span class="line"><span class="string">        &lt;bos&gt; 序列开始符</span></span><br><span class="line"><span class="string">        &lt;eos&gt; 序列结束符</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">for</span> token,freq <span class="keyword">in</span> <span class="variable language_">self</span>._token_freqs:</span><br><span class="line">            <span class="keyword">if</span> freq &lt; min_freq:</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            <span class="keyword">if</span> token <span class="keyword">not</span> <span class="keyword">in</span> <span class="variable language_">self</span>.token_to_idx:</span><br><span class="line">                <span class="variable language_">self</span>.idx_to_token.append(token)</span><br><span class="line">                <span class="variable language_">self</span>.token_to_idx[token] = <span class="built_in">len</span>(<span class="variable language_">self</span>.idx_to_token) - <span class="number">1</span></span><br><span class="line">        </span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__len__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">len</span>(<span class="variable language_">self</span>.idx_to_token)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__getitem__</span>(<span class="params">self, tokens</span>):</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> <span class="built_in">isinstance</span>(tokens, (<span class="built_in">list</span>, <span class="built_in">tuple</span>)):</span><br><span class="line">            <span class="comment">#对单个token的查找，返回他的idx</span></span><br><span class="line">            <span class="keyword">return</span> <span class="variable language_">self</span>.token_to_idx.get(tokens, <span class="variable language_">self</span>.unk)</span><br><span class="line">        </span><br><span class="line">        <span class="comment">#递归查找</span></span><br><span class="line">        <span class="keyword">return</span> [<span class="variable language_">self</span>.__getitem__(token) <span class="keyword">for</span> token <span class="keyword">in</span> tokens]</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">to_tokens</span>(<span class="params">self, indices</span>):</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> <span class="built_in">isinstance</span>(indices, (<span class="built_in">list</span>, <span class="built_in">tuple</span>)):</span><br><span class="line">            <span class="keyword">return</span> <span class="variable language_">self</span>.idx_to_token[indices]</span><br><span class="line">        <span class="keyword">return</span> [<span class="variable language_">self</span>.idx_to_token[index] <span class="keyword">for</span> index <span class="keyword">in</span> indices]</span><br><span class="line">    </span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">unk</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">    </span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">token_freqs</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>._token_freqs</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line"></span><br><span class="line">vocab = Vocab(tokens)</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">list</span>(vocab.token_to_idx.items())[:<span class="number">10</span>])</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">[(&#x27;&lt;unk&gt;&#x27;, 0), (&#x27;the&#x27;, 1), (&#x27;i&#x27;, 2), (&#x27;and&#x27;, 3), (&#x27;of&#x27;, 4), (&#x27;a&#x27;, 5), (&#x27;to&#x27;, 6), (&#x27;was&#x27;, 7), (&#x27;in&#x27;, 8), (&#x27;that&#x27;, 9)]</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">load_corpus_time_machine</span>(<span class="params">max_tokens=-<span class="number">1</span></span>):  <span class="comment">#@save</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;返回时光机器数据集的词元索引列表和词表&quot;&quot;&quot;</span></span><br><span class="line">    lines = read_time_machine()</span><br><span class="line">    tokens = tokenize(lines, <span class="string">&#x27;char&#x27;</span>)</span><br><span class="line">    vocab = Vocab(tokens)</span><br><span class="line">    <span class="comment"># 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落，</span></span><br><span class="line">    <span class="comment"># 所以将所有文本行展平到一个列表中</span></span><br><span class="line">    corpus = [vocab[token] <span class="keyword">for</span> line <span class="keyword">in</span> tokens <span class="keyword">for</span> token <span class="keyword">in</span> line]</span><br><span class="line">    <span class="keyword">if</span> max_tokens &gt; <span class="number">0</span>:</span><br><span class="line">        corpus = corpus[:max_tokens]</span><br><span class="line">    <span class="keyword">return</span> corpus, vocab</span><br><span class="line"></span><br><span class="line">corpus, vocab = load_corpus_time_machine()</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(corpus))</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(vocab))</span><br><span class="line"><span class="built_in">print</span>(corpus[:<span class="number">10</span>])</span><br><span class="line"><span class="built_in">print</span>(vocab[<span class="string">&quot;i&quot;</span>])</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">170580</span></span><br><span class="line"><span class="string">28</span></span><br><span class="line"><span class="string">[3, 9, 2, 1, 3, 5, 13, 2, 1, 13]</span></span><br><span class="line"><span class="string">5</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>以上我们将一段文本-&gt;</p><ul><li>每行一个字符串，构成一个列表</li><li>每个列表token，格式化成列表中的列表</li><li>tokens二维列表 -&gt; 词表（统计索引，idx &#x3D; token的出现频率，且有空，预留token）</li><li>corpus 为索引展平， vocab为词表，里面已经没有顺序关系了。只有频率关系</li></ul><h3 id="语言模型-数据集"><a href="#语言模型-数据集" class="headerlink" title="语言模型+数据集"></a>语言模型+数据集</h3><p><img src="/images/98d1b0c5f68a57964eef2b179a1e2503.jpg" alt="233"></p><p>当给定文本序列{x1, x2, …., xT}, 原来的序列模型，就变成了语言模型了</p><p>语言模型（序列模型）的目标是估计序列的联合概率<code>P(x1, x2, ..., xT)</code></p><p><img src="/images/Pasted%20image%2020260505212148.png"><br>虽然这样的预测下一步，并不能够理解文本，但还是有用的</p><p>现在先对token序列建模，假设在word级别得到词元序列，我们可以依靠前面序列模型的分析，把对<code>P(x1, x2, ..., xT)</code>的预测，拆分成逐步条件概率预测<br><img src="/images/Pasted%20image%2020260505212358.png" alt="398"></p><p><img src="/images/Pasted%20image%2020260505212416.png"></p><p>为了训练语言模型，我们需要<strong>计算单词的概率</strong>， 以及给定前面几个单词后出现某个单词的<strong>条件概率</strong>。 这些概率本质上就是<strong>语言模型的参数</strong></p><p>训练数据集中token的概率，会有很多种设计方式</p><p>一种（稍稍不太精确的）方法是统计单词“deep”在数据集中的<strong>出现次数</strong>， 然后将其<strong>除以</strong>整个语料库中的<strong>单词总数</strong></p><p><img src="/images/Pasted%20image%2020260505212625.png" alt="375">（用了条件概率公式）<br>这种计算数据集概率的方法，当token联合起来之后，概率变得很低，不太靠谱了</p><p>所以考虑拉普拉斯平滑，在计数里面添加一个常量</p><p><img src="/images/Pasted%20image%2020260505212915.png" alt="569"></p><p><mark style="background:#fff88f">模型如果只是简单地统计先前“看到”的单词序列频率， 那么模型面对这种问题肯定是表现不佳的</mark></p><p><img src="/images/Pasted%20image%2020260505213533.png"></p><p>因此，我们不能再用上面的频率来统计token（这样就没有顺序关系了）</p><p>由于序列数据本质上是连续的，因此我们在处理数据时需要解决这个问题</p><blockquote><p>我们在前面用MLP，来实现不断的输入一个序列，不过是并排同时输入一个序列的。</p></blockquote><p>看一下总体策略：<br>假设我们将使用<strong>神经网络来训练语言模型</strong>， 模型中的网络具有一次处理具有预定义长度 （例如n个时间步）的一个小批量序列</p><p>现在的问题是<strong>如何随机生成一个小批量数据的特征和标签以供读取</strong>。</p><ul><li>文本序列是可以任意长的，所以，可以被我们<strong>划分成具有相同时间步的子序列</strong>。</li><li>训练网络时，一个小批量的子序列，被输入模型。</li></ul><p><img src="/images/Pasted%20image%2020260505214408.png" alt="438"></p><ul><li>token &#x3D; char， 一个字符，算一个token。</li><li>n &#x3D; 5, 表示序列长度5个token，表示一次输送5个token进入序列模型，然后预测下一个token<ul><li>就和8.1的序列模型一样{1,2,3,4}-&gt;5_hat</li></ul></li></ul><p><img src="/images/3fdc4e641c56d5e6ef641aee21875c6f.jpg" alt="439"></p><p> 因此，我们可以从<strong>随机偏移量</strong>开始<strong>划分序列</strong>， 以同时获得_<strong>覆盖性</strong><em>（coverage）和</em><strong>随机性_</strong>（randomness）<br><mark style="background:#affad1">-<em>随机采样</em></mark><br>在随机采样中，每个样本都是在原始的长序列上<strong>任意捕获的子序列</strong><br>训练的时候，迭代器给出的先后样本（来自两个相邻的、随机的、<strong>小批量</strong>（batch）中的子序列）不一定在原始序列上相邻</p><p><strong>对于语言建模，目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。</strong></p><p>下面的代码每次可以从数据中随机生成一个小批量。 在这里，参数<code>batch_size</code>指定了每个小批量中子序列样本的数目， 参数<code>num_steps</code>是每个子序列中预定义的时间步数</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">seq_data_iter_random</span>(<span class="params">corpus, batch_size, num_steps</span>):  <span class="comment">#@save</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;使用随机抽样生成一个小批量子序列&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    corpus为语料库的意思, 用token索引，来翻译文本的一个大的list</span></span><br><span class="line"><span class="string">num_steps 为 序列长度（包含多少个token算一个样本）</span></span><br><span class="line"><span class="string">batch_size, 一个batch有多少个样本</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 随机切掉corpus的开头[0, num_steps-1)个token字</span></span><br><span class="line">    corpus = corpus[random.randint(<span class="number">0</span>, num_steps - <span class="number">1</span>):] </span><br><span class="line">    <span class="comment"># 剩下的计算能均分成num_subseqs个num_steps字的样本</span></span><br><span class="line">    num_subseqs = (<span class="built_in">len</span>(corpus) - <span class="number">1</span>) // num_steps</span><br><span class="line">    <span class="comment"># 获得num_subseqs等分的序列样本的开头索引，并随机打乱</span></span><br><span class="line">    initial_indices = <span class="built_in">list</span>(<span class="built_in">range</span>(<span class="number">0</span>, num_subseqs * num_steps, num_steps))</span><br><span class="line">    random.shuffle(initial_indices)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 返回给定开头的序列样本</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">data</span>(<span class="params">pos</span>):</span><br><span class="line">        <span class="comment"># 返回从pos位置开始的长度为num_steps的序列</span></span><br><span class="line">        <span class="keyword">return</span> corpus[pos: pos + num_steps]</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">以上是对原始语料库进行数据处理，处理成样本</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">开始切分batch</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">    num_batches = num_subseqs // batch_size</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, batch_size * num_batches, batch_size):</span><br><span class="line">        <span class="comment"># 准备每个batch的样本集，获得输入特征+标签(下一个）</span></span><br><span class="line">        initial_indices_per_batch = initial_indices[i: i + batch_size]</span><br><span class="line">        X = [data(j) <span class="keyword">for</span> j <span class="keyword">in</span> initial_indices_per_batch]</span><br><span class="line">        Y = [data(j + <span class="number">1</span>) <span class="keyword">for</span> j <span class="keyword">in</span> initial_indices_per_batch]</span><br><span class="line">        <span class="keyword">yield</span> torch.tensor(X), torch.tensor(Y)</span><br></pre></td></tr></table></figure><blockquote><p><strong>corpus是一个按文本顺序，但是用token索引表示的超长列表</strong><br><img src="/images/1d4298c47da1b534c57878f6e63920aa.jpg" alt="474"></p></blockquote><p><img src="/images/cdbc9116ce39fa8ae8094ae00ca4a4c1.jpg" alt="463">（实际的一个样本是这样的）<br><img src="/images/Pasted%20image%2020260505221749.png" alt="334"></p><p><img src="/images/cd4fcf7aae7751f15adc7c0466202850.jpg" alt="523"><br>这里的<strong>语言模型</strong>和前面的<strong>序列模型</strong>，似乎不太一致</p><blockquote><p>我很好奇，这样的数据要如何输入到模型？我在前面序列模型的学习是，假设还是一个样本{13,14,15,16,17} &#x3D; {xt-5, xt-4, xt-3, xt-2,xt-1}, 要来预测xt， 就是输入到MLP里面，把给定的历史输入序列，一次性输入MLP，得到xt_hat.</p></blockquote><p>  但是现在，一个样本是{13,14,15,16,17}-&gt;似乎没有时间步的概念了，按照现在的逻辑是，xt-1 -&gt; xt, 就是 13-&gt; 14, 14-&gt;15的感觉，似乎不一样，序列模型和语言模型</p><p>这正是<strong>全连接网络</strong>（MLP）与<strong>循环神经网络</strong>（RNN）在处理序列数据时最本质的逻辑差异<br><img src="/images/Pasted%20image%2020260505223028.png" alt="512"></p><p><img src="/images/Pasted%20image%2020260505223113.png" alt="546"></p><p><img src="/images/Pasted%20image%2020260505223158.png" alt="622"></p><p><img src="/images/Pasted%20image%2020260505223443.png" alt="463"></p><p><img src="/images/Pasted%20image%2020260505223528.png" alt="630"></p><p><strong>标准的 MLP 序列模型确实不是严格意义上的自回归模型</strong>。</p><p><img src="/images/Pasted%20image%2020260505223742.png" alt="594"><br><img src="/images/Pasted%20image%2020260505223812.png"><br><img src="/images/Pasted%20image%2020260505223818.png"></p><p>前面已经指出，我们构造的数据集，不再是像前面序列模型MLP那样，一口气输入模型，然后预测xt， 现在的挨个输入，挨个预测。<br><img src="/images/Pasted%20image%2020260505223957.png" alt="539"><br>所以，原来的P(14,15,16,17,18) &#x3D; P(14|13) * P(15| 14, 13) * P(16| 15, 14, 13) * P(17| 16, 15,14,13) * P(18 | 17, 16, 15,14, 13) 我们的计算公式。左边是我们的预测目标，右边就是体现的逐个token输入，逐步预测</p><p><img src="/images/Pasted%20image%2020260505224354.png" alt="472"></p><p><img src="/images/Pasted%20image%2020260505224407.png" alt="561"></p><p><mark style="background:#affad1"><em>顺序分区</em></mark><br><strong>顺序分区（Sequential Partitioning）</strong> 是另一种从长序列语料库中采样的方法</p><p>它保证了<strong>相邻的小批量（Mini-batch）在时间顺序上是连续的</strong></p><p>核心逻辑：保留“跨 Batch”的记忆</p><p>在顺序分区中，迭代过程中两个相邻的batch中的子序列在原始序列上也是相邻的。<br><strong>小批量 1</strong> 结束后，<strong>小批量 2</strong> 会紧接着从小批量 1 停止的地方开始读取。</p><p><strong>目的</strong>：这样做是为了在训练循环神经网络（RNN）时，可以将上一个小批量的最终隐藏状态（Hidden State）直接传递给下一个小批量，从而让模型学习到比 <code>num_steps</code> 更长的依赖关系</p><p><img src="/images/Pasted%20image%2020260505224645.png"></p><h3 id="循环神经网络RNN"><a href="#循环神经网络RNN" class="headerlink" title="循环神经网络RNN"></a>循环神经网络RNN</h3><p><img src="/images/Pasted%20image%2020260505224829.png"></p><p>开始引入隐变量</p><p><strong>从一开始的考虑所有的历史输入： P(xt| xt-1, …, x1)</strong><br>随着时间推移，历史序列会无限变长，计算量和参数量会呈指数级爆炸<br>-&gt; 马尔可夫n阶，<strong>n元语法模型： P(xt | xt-1, …, xt-n+1)</strong><br><strong>马尔可夫性质</strong>。模型假设“只有最近的 $n-1$ 个词对现在有影响”，再久远的过去都可以忽略不计<br>-&gt; <strong>隐变量模型： P(xt | ht-1)</strong></p><ul><li>$h_{t-1}$ 就像是一个“压缩包”，它试图把从 $x_1$ 到 $x_{t-1}$ 的所有有用信息都揉进一个向量里。</li><li>预测 $x_t$ 时，模型只需要看一眼这个“压缩包”即可。<br><img src="/images/Pasted%20image%2020260505225004.png"></li></ul><p>其中，ht-1, 叫做隐状态&#x2F;隐藏变量， 他存储了到t-1时间步的序列信息。</p><p><strong>隐藏层</strong><br>隐藏层是在从输入到输出的路径上（以观测角度来理解）的隐藏的层<br><strong>隐状态</strong><br>而隐状态则是在给定步骤所做的任何事情（以技术角度来定义）的_输入_，并且这些状态只能通过先前时间步的数据来计算</p><p><mark style="background:#fff88f"><em>循环神经网络</em>（recurrent neural networks，RNNs） 是具有隐状态的神经网络</mark></p><p>MLP是没有隐状态的神经网络</p><p><strong>有隐状态的循环神经网络</strong><br>有了隐状态之后，情况完全不一样</p><h3 id="最终理解"><a href="#最终理解" class="headerlink" title="最终理解"></a>最终理解</h3><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> math</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> torch.nn <span class="keyword">import</span> functional <span class="keyword">as</span> F</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"><span class="keyword">import</span> time_machine_loader <span class="keyword">as</span> my</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">构造数据集</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">lines = my.read_time_machine()</span><br><span class="line">tokens = d2l.tokenize(lines, token=<span class="string">&#x27;char&#x27;</span>)</span><br><span class="line">vocab = d2l.Vocab(tokens, min_freq=<span class="number">0</span>, reserved_tokens=[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>])</span><br><span class="line"><span class="comment"># 按频率编码, 总共29个索引编码（a-z, , &lt;unk&gt;,）</span></span><br><span class="line"><span class="comment"># vocab，字典&#123;(token, ID)&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 用ID表示文本</span></span><br><span class="line">corpus = [vocab[token] <span class="keyword">for</span> line <span class="keyword">in</span> tokens <span class="keyword">for</span> token <span class="keyword">in</span> line] </span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;======&quot;</span>, <span class="built_in">len</span>(corpus)) <span class="comment"># 170580</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 35个token=一个样本，32个样本=一个batch</span></span><br><span class="line">batch_size = <span class="number">32</span> </span><br><span class="line">num_steps = <span class="number">35</span> <span class="comment"># 样本长度</span></span><br><span class="line">dataset = my.TimeMachineDataset(corpus, num_steps)</span><br><span class="line"><span class="comment"># 样本= &lt;class &#x27;tuple&#x27;&gt; X =  torch.Size([35]) Y =  torch.Size([35])</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;=====dataset&quot;</span>, <span class="built_in">len</span>(dataset), <span class="string">&quot;样本=&quot;</span>, <span class="built_in">type</span>(dataset[<span class="number">0</span>]),<span class="string">&quot;X = &quot;</span>, dataset[<span class="number">0</span>][<span class="number">0</span>].shape,<span class="string">&quot;Y = &quot;</span>, dataset[<span class="number">0</span>][<span class="number">1</span>].shape) <span class="comment"># 170580 / 35 = 4873, dataset一共包含4873个样本序列</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 构造迭代器，32个样本为一个batch，随机抽取，不足的丢弃</span></span><br><span class="line">train_iter = my.DataLoader(dataset, batch_size=batch_size, shuffle=<span class="literal">True</span>,drop_last=<span class="literal">True</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;======&quot;</span>, <span class="built_in">len</span>(train_iter)) <span class="comment"># 4872 / 32 = 152个batch</span></span><br><span class="line"><span class="keyword">for</span> (X,Y) <span class="keyword">in</span> train_iter:</span><br><span class="line">    <span class="comment"># (batch_size, seq_len), 每个token目前还是ID</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;X.shape=&quot;</span>, X.shape) <span class="comment"># X.shape= torch.Size([32, 35])</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># batch_size = 2, seq_len = 5</span></span><br><span class="line"><span class="comment"># X(2,5)(batch_size, seq_len)</span></span><br><span class="line">X = torch.arange(<span class="number">10</span>).reshape(<span class="number">2</span>,<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试：(5,2,29)=(batch_size, seq_len，input_size)</span></span><br><span class="line"><span class="built_in">print</span>(F.one_hot(X.T, <span class="built_in">len</span>(vocab)).shape)  </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"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">创建模型参数的矩阵，向量, 定义了模型的结构</span></span><br><span class="line"><span class="string">（input_size, hidden_size, device）-&gt; gpu</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_params</span>(<span class="params">vocab_size,num_hiddens,device</span>):</span><br><span class="line">    num_inputs = num_outputs = vocab_size</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">normal</span>(<span class="params">shape</span>):</span><br><span class="line">        <span class="keyword">return</span> torch.randn(size = shape, device = device)* <span class="number">0.01</span></span><br><span class="line"></span><br><span class="line">    W_xh = normal((num_inputs, num_hiddens))</span><br><span class="line">    W_hh = normal((num_hiddens, num_hiddens))</span><br><span class="line">    b_h = torch.zeros(num_hiddens, device=device)</span><br><span class="line"></span><br><span class="line">    W_hq= normal((num_hiddens, num_outputs))</span><br><span class="line">    b_q = torch.zeros(num_outputs, device=device)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># rnn的参数列表</span></span><br><span class="line">    params = [W_xh, W_hh, b_h, W_hq, b_q]</span><br><span class="line">    <span class="keyword">for</span> param <span class="keyword">in</span> params:</span><br><span class="line">        param.requires_grad_(<span class="literal">True</span>) <span class="comment"># 标记为需要求导</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> params</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 返回一个0的一个批量并行计算的 H0</span></span><br><span class="line"><span class="comment"># H0 (batch_size, hidden_size) -&gt; gpu</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">init_rnn_state</span>(<span class="params">batch_size, num_hiddens, device</span>):</span><br><span class="line">    <span class="keyword">return</span> (torch.zeros((batch_size, num_hiddens), device=device),)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义一个batch的所有样本一次前向传播的计算, state为H， params为模型参数，inputs为X:(seq_len,batch_size, input_size)</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">inputs, 一个batch的数据:(seq_len, batch_size, input_size)</span></span><br><span class="line"><span class="string">params = 模型的参数矩阵</span></span><br><span class="line"><span class="string">state = 当前的模型的隐变量H0</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">run</span>(<span class="params">inputs, state, params</span>):</span><br><span class="line">    W_xh, W_hh, b_h, W_hq, b_q, = params</span><br><span class="line">    H, = state</span><br><span class="line">    outputs = []</span><br><span class="line"></span><br><span class="line">    <span class="comment"># X为batch的所有样本的t时刻的输入,X = Xt:(batch_size, input_size)</span></span><br><span class="line">    <span class="comment"># H为batch的所有样本的t时刻的隐状态，H = Ht:(batch_size, hidden_size)</span></span><br><span class="line">    <span class="comment"># Y为该batch的所有样本的t时刻的输出，Y = Yt:(batch_size, input_size)</span></span><br><span class="line">    <span class="comment"># outputs:(seq_len, batch_size, input_size)</span></span><br><span class="line">    <span class="comment"># return: (seq_len*batch_size, input_size), ((batch_size, hidden_size),)</span></span><br><span class="line">    <span class="keyword">for</span> X <span class="keyword">in</span> inputs:</span><br><span class="line">        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)</span><br><span class="line">        Y = torch.mm(H, W_hq) + b_q</span><br><span class="line">        outputs.append(Y) <span class="comment"># Y为batch所有样本t时刻的预测输出</span></span><br><span class="line">        </span><br><span class="line">    <span class="keyword">return</span> torch.cat(outputs, dim=<span class="number">0</span>), (H,)</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">inputs: 一个batch的所有样本序列 (seq_len, batch_size, input_size)</span></span><br><span class="line"><span class="string">state: batch中每个独立样本对应的ht隐状态的矩阵(batch_size, num_hiddens)</span></span><br><span class="line"><span class="string">outputs: 一个batch的所有样本序列全部预测完之后的输出(seq_len, batch_size, input_size)</span></span><br><span class="line"><span class="string">H = 最后的隐状态</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;RNN 模型</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">vocab_size = input_size</span></span><br><span class="line"><span class="string">num_hiddens = hidden_size</span></span><br><span class="line"><span class="string">get_params = 模型参数</span></span><br><span class="line"><span class="string">init_state = H0初始隐状态(batch_size, hidden_size)</span></span><br><span class="line"><span class="string">forward_fn 输入一个batch的数据</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RNNModelScratch</span>:</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn</span>):</span><br><span class="line">        <span class="variable language_">self</span>.vocab_size, <span class="variable language_">self</span>.num_hiddens = vocab_size, num_hiddens</span><br><span class="line">        <span class="variable language_">self</span>.params = get_params(vocab_size, num_hiddens, device)</span><br><span class="line">        <span class="variable language_">self</span>.init_state = init_state</span><br><span class="line">        <span class="variable language_">self</span>.forward_fn = forward_fn</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    模型调用model(X, state)</span></span><br><span class="line"><span class="string">    X 一个batch的完整数据（未hot编码）(batch_size, seq_len)</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__call__</span>(<span class="params">self, X, state</span>):</span><br><span class="line">        </span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        X: (seq_len, batch_size, input_size)</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        X = F.one_hot(X.T, <span class="variable language_">self</span>.vocab_size).<span class="built_in">type</span>(torch.float32)</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.forward_fn(X, state, <span class="variable language_">self</span>.params)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 获得H0 = 0</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">begin_state</span>(<span class="params">self, batch_size, device</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.init_state(batch_size, <span class="variable language_">self</span>.num_hiddens, device)</span><br><span class="line"></span><br><span class="line">device = torch.device(<span class="string">&quot;cuda&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># hidden_size = 512</span></span><br><span class="line">num_hiddens = <span class="number">512</span></span><br><span class="line">net = RNNModelScratch(<span class="built_in">len</span>(vocab), num_hiddens=num_hiddens,</span><br><span class="line">                      device=device,get_params=get_params,</span><br><span class="line">                      init_state=init_rnn_state, forward_fn=run )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># (batch_size, seq_len)每个元素是一个ID</span></span><br><span class="line"><span class="built_in">print</span>(X.shape)</span><br><span class="line"></span><br><span class="line"><span class="comment"># state = (batch_szie, hidden_size) = 0 (H0)</span></span><br><span class="line">state = net.begin_state(X.shape[<span class="number">0</span>], device=device)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 前向传播（x-&gt;gpu）X为随机生成的，state=H0,为0矩阵</span></span><br><span class="line"><span class="comment"># Y=(seq_len*batch_size, input_size), new_state=((batch_size, hidden_size),)</span></span><br><span class="line">Y, new_state = net(X.to(device=device), state)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;net return outputs [seq_len*batch_size, input_size]&quot;</span>, Y.shape)</span><br><span class="line"><span class="built_in">print</span>(new_state)</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 class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">预测函数</span></span><br><span class="line"><span class="string">prefix:给定输入序列</span></span><br><span class="line"><span class="string">num_preds，自回归多少步</span></span><br><span class="line"><span class="string">net: 模型</span></span><br><span class="line"><span class="string">vocab: 词表，用来翻译prefix</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">predict_ch8</span>(<span class="params">prefix, num_preds, net, vocab, device</span>):</span><br><span class="line">    <span class="comment"># H0 = (batch_size, hidden_size) = (1, 512)</span></span><br><span class="line">    state = net.begin_state(batch_size = <span class="number">1</span>, device=device)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 把prefix的第0个token放入list， 定义匿名lambda，返回outputs最后一个token的IDtensor</span></span><br><span class="line">    outputs = [vocab[prefix[<span class="number">0</span>]]]</span><br><span class="line">    get_input = <span class="keyword">lambda</span>: torch.tensor([outputs[-<span class="number">1</span>]], device=device).reshape((<span class="number">1</span>, <span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 先更新隐状态，模型参数目前是随机的</span></span><br><span class="line">    <span class="comment"># 预热，目的是更新H状态，不要让他处于H0的0, 同时也让给定的序列加入我们最终的输出</span></span><br><span class="line">    <span class="comment"># y 是一个 token</span></span><br><span class="line">    <span class="keyword">for</span> y <span class="keyword">in</span> prefix[<span class="number">1</span>:]:</span><br><span class="line">        <span class="comment"># 模型net基于上一个，和当前的状态，预测,_ = (1*1,input_size), state=((1, hidden_size),)</span></span><br><span class="line">        _, state = net(get_input(), state)</span><br><span class="line">        <span class="comment"># 把标签保存下来</span></span><br><span class="line">        outputs.append(vocab[y])</span><br><span class="line"></span><br><span class="line">    <span class="comment"># state已经被更新过好几次了，y = (1*1,input_size)</span></span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(num_preds): <span class="comment">#预测num_preds步</span></span><br><span class="line">        y, state = net(get_input(), state) <span class="comment"># 这边是做真正的自回归</span></span><br><span class="line">        outputs.append(<span class="built_in">int</span>(y.argmax(dim=<span class="number">1</span>).reshape(<span class="number">1</span>))) <span class="comment"># argmax从hot编码得到IDX</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 返回整个自回归的序列</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;&quot;</span>.join([vocab.idx_to_token[i] <span class="keyword">for</span> i <span class="keyword">in</span> outputs])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">result = predict_ch8(<span class="string">&quot;time traveller &quot;</span>, <span class="number">10</span>, net, vocab, device=device)</span><br><span class="line"><span class="built_in">print</span>(result)</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"></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 class="string">&quot;&quot;&quot;梯度裁剪</span></span><br><span class="line"><span class="string">仅能解决梯度爆炸</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">grad_clipping</span>(<span class="params">net, theta</span>):</span><br><span class="line">    <span class="comment"># 如果是继承的基类，就直接用基类的方法返回要求梯度的参数</span></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(net, nn.Module):</span><br><span class="line">        params = [p <span class="keyword">for</span> p <span class="keyword">in</span> net.parameters() <span class="keyword">if</span> p.requires_grad]</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="comment"># 自定义的类，直接访问公共的参数列表</span></span><br><span class="line">        params = net.params</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># params = [Wxh, Whh, Whq, b_h, b_q]</span></span><br><span class="line">    <span class="comment"># 每个模型参数都有梯度， d(Loss)/d(Wxh).shape = Wxh.shape, </span></span><br><span class="line">    <span class="comment"># 参数矩阵里面每个元素，都有梯度，所以梯度张量的shape = 参数张量的shape</span></span><br><span class="line">    <span class="comment"># 单个参数梯度的各个元素的梯度平方和，然后所有参数的梯度全部加起来，开方</span></span><br><span class="line">    norm = torch.sqrt(<span class="built_in">sum</span>(torch.<span class="built_in">sum</span>((p.grad**<span class="number">2</span>)) <span class="keyword">for</span> p <span class="keyword">in</span> params))</span><br><span class="line">    <span class="comment"># &gt; 阈值，容易发送梯度爆炸</span></span><br><span class="line">    <span class="keyword">if</span> norm &gt; theta:</span><br><span class="line">        <span class="keyword">for</span> param <span class="keyword">in</span> params:</span><br><span class="line">            param.grad[:] *= theta/norm <span class="comment">#对每个参数的梯度，进行压缩</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;训练一个epoch</span></span><br><span class="line"><span class="string">net: 模型</span></span><br><span class="line"><span class="string">train_iter: 训练集迭代器</span></span><br><span class="line"><span class="string">loss：损失函数对象</span></span><br><span class="line"><span class="string">updater: 优化器(每个batch，H的计算图也牵涉了其他batch，需要detach清除计算图)</span></span><br><span class="line"><span class="string">use_random_iter: 随机采样（每个batch开始要清空H隐状态）， 否则是顺序分区（无需清空H）</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">train_epoch_ch8</span>(<span class="params">net, train_iter, loss, updater, device, use_random_iter</span>):</span><br><span class="line">    state, timer = <span class="literal">None</span>, d2l.Timer()</span><br><span class="line">    metric = d2l.Accumulator(<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 每个for一个batch</span></span><br><span class="line">    <span class="comment"># X (batch_size, seq_len)， Y(batch_size, seq_len)</span></span><br><span class="line">    <span class="keyword">for</span> (X, Y) <span class="keyword">in</span> train_iter:</span><br><span class="line">        <span class="comment"># state = None, 说明是第一个batch</span></span><br><span class="line">        <span class="comment"># use_random_iter=True, 说明是随机采样，需要每个batch开头，重置H0</span></span><br><span class="line">        <span class="keyword">if</span> state <span class="keyword">is</span> <span class="literal">None</span> <span class="keyword">or</span> use_random_iter:</span><br><span class="line">            <span class="comment"># 在第一次迭代或使用随机抽样时初始化state = H0</span></span><br><span class="line">            state = net.begin_state(batch_size=X.shape[<span class="number">0</span>], device=device)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="comment"># 不是第一次，且顺序分区</span></span><br><span class="line">            <span class="comment"># 如果是官方的net对象，并且，state此时不是元组，（就是官方的GRU）</span></span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">isinstance</span>(net, nn.Module) <span class="keyword">and</span> <span class="keyword">not</span> <span class="built_in">isinstance</span>(state, <span class="built_in">tuple</span>):</span><br><span class="line">                state.detach_()</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="comment"># 我们自定义/官方的LSTM</span></span><br><span class="line">                <span class="comment"># 每个batch，我们都要断开该batch的所有样本的隐状态的梯度图，清除上一个batch的隐状态的计算图</span></span><br><span class="line">                <span class="comment"># state = (batch_size, hidden_size)</span></span><br><span class="line">                <span class="keyword">for</span> s <span class="keyword">in</span> state:</span><br><span class="line">                    s.detach_() <span class="comment"># s = ht(某个样本，上一个batch留下来的ht(1,hidden_size))</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 标签y=(seq_len * batch_size)，里面元素是ID</span></span><br><span class="line">        y = Y.T.reshape(-<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 训练集(X,y)-&gt;gpu</span></span><br><span class="line">        X , y = X.to(device), y.to(device)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># X在net里面被转成了(seq_len, batch_size, input_size)</span></span><br><span class="line">        <span class="comment"># y_hat=(seq_len*batch_size, input_size), state元组=((batch_size, hidden_size),)</span></span><br><span class="line">        y_hat, state = net(X, state)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 交叉熵损失函数(二维张量o，标签值)</span></span><br><span class="line">        <span class="comment"># y-&gt;hat -&gt; 哪个词的概率分布，词表的ID就是哪个词的ID</span></span><br><span class="line">        <span class="comment"># loss得到（seq_len*batch_size,）个loss（一个token一个loss）</span></span><br><span class="line">        <span class="comment"># .mean，求均值</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        y_hat 经过 softmax 后 = [0.1, 0.05, 0.02, ... , 0.07]  一共29个概率，加起来=1</span></span><br><span class="line"><span class="string">        y = 真实 ID = 5</span></span><br><span class="line"><span class="string">        就是MLP分类里面的softmax回归</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        l =loss(y_hat, y.long()).mean()</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 如果指定的官方优化器</span></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">isinstance</span>(updater, torch.optim.Optimizer):</span><br><span class="line">            updater.zero_grad()</span><br><span class="line">            l.backward()</span><br><span class="line">            grad_clipping(net, <span class="number">1</span>)</span><br><span class="line">            updater.step()</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="comment"># 自己定义的优化器</span></span><br><span class="line">            l.backward()</span><br><span class="line">            grad_clipping(net, <span class="number">1</span>)</span><br><span class="line">            updater(batch_size=<span class="number">1</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># l是这个batch的平均损失， y.numel()获得batch的token个数</span></span><br><span class="line">        <span class="comment"># metric是自定义的工具类，metric[0] 总loss, metric[1], token个数</span></span><br><span class="line">        metric.add(l * y.numel(), y.numel())</span><br><span class="line"></span><br><span class="line">    <span class="comment"># e^(平均损失) = 困惑度</span></span><br><span class="line">    <span class="keyword">return</span> math.exp(metric[<span class="number">0</span>] / metric[<span class="number">1</span>]), metric[<span class="number">1</span>]/timer.stop()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#@save</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">多个epoch训练</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">train_ch8</span>(<span class="params">net, train_iter, vocab, lr, num_epochs, device,</span></span><br><span class="line"><span class="params">              use_random_iter=<span class="literal">False</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;训练模型（定义见第8章）&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 交叉熵损失</span></span><br><span class="line">    loss = nn.CrossEntropyLoss()</span><br><span class="line">    animator = d2l.Animator(xlabel=<span class="string">&#x27;epoch&#x27;</span>, ylabel=<span class="string">&#x27;perplexity&#x27;</span>,</span><br><span class="line">                            legend=[<span class="string">&#x27;train&#x27;</span>], xlim=[<span class="number">10</span>, num_epochs])</span><br><span class="line">    <span class="comment"># 初始化</span></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(net, nn.Module):</span><br><span class="line">        updater = torch.optim.SGD(net.parameters(), lr)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="comment"># 我们自定义的优化器</span></span><br><span class="line">        updater = <span class="keyword">lambda</span> batch_size: d2l.sgd(net.params, lr, batch_size)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义一个函数predict</span></span><br><span class="line">    predict = <span class="keyword">lambda</span> prefix: predict_ch8(prefix, <span class="number">50</span>, net, vocab, device)</span><br><span class="line">    <span class="comment"># 训练和预测</span></span><br><span class="line">    <span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(num_epochs):</span><br><span class="line">        ppl, speed = train_epoch_ch8(</span><br><span class="line">            net, train_iter, loss, updater, device, use_random_iter)</span><br><span class="line">        <span class="keyword">if</span> (epoch + <span class="number">1</span>) % <span class="number">10</span> == <span class="number">0</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;epoch=&quot;</span>, epoch)</span><br><span class="line">            <span class="built_in">print</span>(predict(<span class="string">&#x27;time traveller&#x27;</span>))</span><br><span class="line">            animator.add(epoch + <span class="number">1</span>, [ppl])</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&#x27;困惑度 <span class="subst">&#123;ppl:<span class="number">.1</span>f&#125;</span>, <span class="subst">&#123;speed:<span class="number">.1</span>f&#125;</span> 词元/秒 <span class="subst">&#123;<span class="built_in">str</span>(device)&#125;</span>&#x27;</span>)</span><br><span class="line">    <span class="built_in">print</span>(predict(<span class="string">&#x27;time traveller&#x27;</span>))</span><br><span class="line">    <span class="built_in">print</span>(predict(<span class="string">&#x27;traveller&#x27;</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">num_epochs = <span class="number">500</span></span><br><span class="line">lr = <span class="number">1</span></span><br><span class="line">train_ch8(net, train_iter=train_iter, vocab=vocab, lr=lr, num_epochs=num_epochs, device=device)</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="BPTT-通过时间反向传播"><a href="#BPTT-通过时间反向传播" class="headerlink" title="BPTT 通过时间反向传播"></a>BPTT 通过时间反向传播</h3><p><img src="/images/847aa0a58ce607f545656c6510a21cbe.jpg"></p><p><img src="/images/Pasted%20image%2020260506195707.png"></p><h2 id="现代循环神经网络"><a href="#现代循环神经网络" class="headerlink" title="现代循环神经网络"></a>现代循环神经网络</h2><h3 id="GRU-门控循环单元"><a href="#GRU-门控循环单元" class="headerlink" title="GRU 门控循环单元"></a>GRU 门控循环单元</h3><p><img src="/images/0849c799212bdc8149c76a05f74d194f.jpg"></p><h3 id="LSTM-长短期记忆网络"><a href="#LSTM-长短期记忆网络" class="headerlink" title="LSTM 长短期记忆网络"></a>LSTM 长短期记忆网络</h3><p><img src="/images/e41d10cc82fb008c654be33927cbb193.jpg" alt="480"></p><h3 id="深度循环神经网络"><a href="#深度循环神经网络" class="headerlink" title="深度循环神经网络"></a>深度循环神经网络</h3><p>前面<strong>RNN</strong>，就是一个普通的隐状态层，他利用输入+上一时刻的本层的隐状态来得到<br>后面提到了<strong>GRU，LSTM</strong>，就是优化了这个隐藏层的设计，加入了一些门控的设计，让隐状态Ht的计算变得复杂，本质结构并没有变化，都是<mark style="background:#fff88f">针对单层而言的。</mark></p><p>现在的深度循环神经网络，是通过在向后，增加到多层，后面层的隐藏层，不仅以来前一层（第一层就是输入）的隐状态输出，还有自己本层的上一时刻的隐状态。</p><p><img src="/images/Pasted%20image%2020260507151433.png" alt="303"><br>网络时移图如上所示。</p><ul><li>加入了门控机制：更好的捕捉时间步距离很长的序列的依赖关系<ul><li>GRU：<ul><li>重置门：捕捉短期依赖</li><li>更新门：捕捉长期依赖</li></ul></li></ul></li><li>多层的深度设计：解决了单层只能学习输入数据和隐藏层一层之间的非线性映射关系。复杂语义不行的问题。<ul><li>每一层对上一层进行加工<ul><li>浅层：低级特征（语法，词法，简单组合）</li><li>深层：高级特征（长文本的逻辑结构，情感转折，语义内涵）</li></ul></li></ul></li></ul><p><mark style="background:#fff88f">数学上已经证明，深层网络可以用比浅层网络更少的神经元，去拟合极其复杂的函数。</mark></p><p><img src="/images/Pasted%20image%2020260507151856.png" alt="545"></p><h3 id="编码器解码器架构"><a href="#编码器解码器架构" class="headerlink" title="编码器解码器架构"></a>编码器解码器架构</h3><p><img src="/images/Pasted%20image%2020260507111716.png" alt="451"></p><p><strong>解决的问题是</strong>：输入输出都是长度可变的序列，我们要把可变长度的输入，通过编码器转成固定长度的状态编码，然后输入到解码器，解码器将这个状态映射到长度可变的序列。</p><p>在编码器-解码器（Encoder-Decoder）架构中，<strong>这两个“输入”有着完全不同的功能和来源</strong></p><p><mark style="background:#fff88f">1. 编码器的输入 (Encoder Input)</mark></p><ul><li><strong>来源：</strong> 这是我们要处理的<strong>源序列</strong>（Source Sequence）。</li><li><strong>内容：</strong> 比如在机器翻译中，如果你想把中文翻译成英文，编码器的输入就是<strong>完整的中文句子</strong>（如：“我爱人工智能”）。</li><li><strong>作用：</strong> 编码器负责将这个原始输入“压缩”成一个包含语义信息的固定形状的<strong>状态</strong>（Context Vector）。</li></ul><p><mark style="background:#fff88f">2. 解码器的输入 (Decoder Input)</mark></p><ul><li><strong>来源：</strong> 在<strong>训练阶段</strong>和<strong>推理（预测）阶段</strong>略有不同。</li><li><strong>内容：</strong> 它通常是<strong>目标序列</strong>（Target Sequence）的已知部分。<ul><li><strong>在训练时（Teacher Forcing）：</strong> 解码器的输入是翻译后的<strong>正确英文句子</strong>（如：“I love AI”），但会做一个偏移（Shift），让模型学习根据已经出现的单词预测下一个单词。</li><li><strong>在预测（推理）时：</strong> 解码器的输入是<strong>上一个时间步自己生成的单词</strong>。</li></ul></li><li><strong>作用：</strong> 解码器需要根据编码器提供的“状态”以及它“目前已经生成了什么”，来决定下一个词该输出什么。</li></ul><table><thead><tr><th><strong>特性</strong></th><th><strong>编码器输入</strong></th><th><strong>解码器输入</strong></th></tr></thead><tbody><tr><td><strong>所属序列</strong></td><td>源序列 (Source)</td><td>目标序列 (Target)</td></tr><tr><td><strong>语言</strong></td><td>通常是语言 A (如中文)</td><td>通常是语言 B (如英文)</td></tr><tr><td><strong>时间点</strong></td><td>一次性输入完整序列</td><td>随时间步逐个输入（Autoregressive）</td></tr><tr><td><img src="/images/Pasted%20image%2020260507112139.png" alt="510"></td><td></td><td></td></tr></tbody></table><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment">#@save</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">EncoderDecoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基类&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, encoder, decoder, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(EncoderDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.encoder = encoder</span><br><span class="line">        <span class="variable language_">self</span>.decoder = decoder</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, enc_X, dec_X, *args</span>):</span><br><span class="line">        enc_outputs = <span class="variable language_">self</span>.encoder(enc_X, *args)</span><br><span class="line">        dec_state = <span class="variable language_">self</span>.decoder.init_state(enc_outputs, *args)</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.decoder(dec_X, dec_state)</span><br></pre></td></tr></table></figure><h3 id="序列到序列学习"><a href="#序列到序列学习" class="headerlink" title="序列到序列学习"></a>序列到序列学习</h3><p>利用编码器解码器结构+深层循环神经网络实现机器翻译问题：</p><ul><li>输入：a长度的序列（英语）</li><li>隐状态：固定长度的状态向量</li><li>输出标签：b长度的序列（法语）</li></ul><p>这里，我们使用两个循环神经网络的编码器解码器。来实现翻译</p><p>这里有几个设计点：</p><ul><li>编码器本身是一个循环神经网络，解码器本身也是一个独立的循环神经网络</li><li>特定的”bos”表示序列开始词元，它是解码器的输入序列的第一个词元</li><li>解码器循环神经网络，一旦预测“eos”出就停止预测</li><li>使用循环神经网络<mark style="background:#fff88f">编码器最终的隐状态</mark>来初始化解码器的隐状态</li></ul><p><img src="/images/Pasted%20image%2020260507153724.png" alt="501"><br><img src="/images/434a1f10da21a0a72506790b7e987d96.jpg" alt="442"></p><h4 id="编码器"><a href="#编码器" class="headerlink" title="编码器"></a>编码器</h4><p>这个循环神经网络，我们使用的是单向循环神经网络：</p><p>假设输入的batch_size &#x3D; 1,  输入序列{x1, x2,… , xT}</p><p>状态转移如下：<br><img src="/images/Pasted%20image%2020260507160907.png" alt="158"></p><p>上下文变量就是用q方法，得到的向量，他是依赖所有的状态量得到的<br><img src="/images/Pasted%20image%2020260507160927.png" alt="468"></p><p>所以，如果选择q(h1, …, hT) &#x3D; hT时，<strong>上下文变量</strong>就是最后的hT</p><p>我们使用的是一个<strong>单向循环神经网络</strong>来<strong>设计编码器</strong></p><p>注意，我们使用了_<strong>嵌入层</strong>_（embedding layer） 来获得输入序列中<strong>每个词元的特征向量</strong></p><p> <strong>嵌入层</strong>的<strong>权重</strong>是一个矩阵， <strong>其行数等于输入词表的大小</strong>（<code>vocab_size</code>）</p><p>其<strong>列数</strong>等于<strong>特征向量的维度</strong>（<code>embed_size</code>）<br><img src="/images/5f131d34f96c2ba5135cc6efc90ad39e.jpg" alt="327"></p><p><strong>本文选择了一个多层门控循环单元来实现编码器</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">import</span> math</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"> </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Encoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基本编码器接口&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Encoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, *args</span>):</span><br><span class="line">        <span class="keyword">raise</span> NotImplementedError</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#@save</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Seq2SeqEncoder</span>(<span class="title class_ inherited__">Encoder</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;用于序列到序列学习的循环神经网络编码器&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embed_size, num_hiddens, num_layers,</span></span><br><span class="line"><span class="params">                 dropout=<span class="number">0</span>, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Seq2SeqEncoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="comment"># 嵌入层(embed_size = 特征向量维度 = input_size)</span></span><br><span class="line">        <span class="variable language_">self</span>.embedding = nn.Embedding(vocab_size, embed_size)</span><br><span class="line">        <span class="variable language_">self</span>.rnn = nn.GRU(embed_size, num_hiddens, num_layers,</span><br><span class="line">                          dropout=dropout)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, *args</span>):</span><br><span class="line">        <span class="comment"># 输出&#x27;X&#x27;的形状：(batch_size,num_steps,embed_size)</span></span><br><span class="line">        X = <span class="variable language_">self</span>.embedding(X)</span><br><span class="line">        <span class="comment"># 在循环神经网络模型中，第一个轴对应于时间步,(num_steps,batch_size,embed_size)</span></span><br><span class="line">        X = X.permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 如果未提及状态，则默认为0</span></span><br><span class="line">        output, state = <span class="variable language_">self</span>.rnn(X)</span><br><span class="line">        <span class="comment"># output的形状:(num_steps,batch_size,num_hiddens)</span></span><br><span class="line">        <span class="comment"># state的形状:(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">        <span class="keyword">return</span> output, state</span><br><span class="line">        <span class="comment"># 这个state, 返回的是所有隐藏层的最后一个时间步的状态</span></span><br><span class="line">    </span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">编码器循环神经网络：</span></span><br><span class="line"><span class="string">10种token, input_size=8, hiddens_size=16, 隐藏层2层</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">encoder = Seq2SeqEncoder(vocab_size=<span class="number">10</span>, embed_size=<span class="number">8</span>, num_hiddens=<span class="number">16</span>,</span><br><span class="line">                         num_layers=<span class="number">2</span>)</span><br><span class="line">encoder.<span class="built_in">eval</span>() <span class="comment"># 设置该隐藏层可以修改参数</span></span><br><span class="line">X = torch.zeros((<span class="number">4</span>, <span class="number">7</span>), dtype=torch.long) <span class="comment"># X= (batch_size, seq_len)，元素为ID</span></span><br><span class="line">output, state = encoder(X)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(output.shape)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260507162531.png" alt="199"><br>以上，我们已经实现了编码器这一部分了，最后输出的这个state, 返回的是<strong>所有隐藏层</strong>的<strong>最后一个时间步的状态</strong>， 也就是我们编码器输出的上下文变量c<br><img src="/images/Pasted%20image%2020260507160907.png" alt="158"><br><img src="/images/Pasted%20image%2020260507160927.png" alt="468"></p><h4 id="解码器"><a href="#解码器" class="headerlink" title="解码器"></a>解码器</h4><p>来自训练数据集的输出序列{y1, y2,…, yT’}, 对于每个时间步t‘, 解码器输出yt’的预测概率取决于前面的输入，以及上下文变量c<br><img src="/images/Pasted%20image%2020260507163119.png" alt="242"></p><p>为了在序列上<strong>模型化这种条件概率</strong>， 我们可以使用<strong>另一个循环神经网络</strong>作为<strong>解码器</strong></p><p><img src="/images/188f7d0c9a4e3340ea757afe459d5c81.jpg" alt="390"></p><p>所以，状态转移为：<img src="/images/Pasted%20image%2020260507163510.png"></p><p>当实现解码器时， 我们直接使用<strong>编码器最后一个时间步的隐状态</strong>来<strong>初始化解码器的隐状态</strong></p><p>所以<code>s0 = hT</code></p><p>所以，这就要求，编码器和解码器的状态h&#x3D;s，都是同尺寸，同层数，因为编码器返回的状态是最后一步的所有层的状态：(num_layers,batch_size,num_hiddens)</p><p>为了预测输出词元的概率分布， 在循环神经网络<strong>解码器的最后一层</strong>使用<strong>全连接层来变换隐状态</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Seq2SeqDecoder</span>(<span class="title class_ inherited__">Decoder</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;用于序列到序列学习的循环神经网络解码器&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embed_size, num_hiddens, num_layers,</span></span><br><span class="line"><span class="params">                 dropout=<span class="number">0</span>, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Seq2SeqDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.embedding = nn.Embedding(vocab_size, embed_size)</span><br><span class="line">        <span class="variable language_">self</span>.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,</span><br><span class="line">                          dropout=dropout)</span><br><span class="line">        <span class="variable language_">self</span>.dense = nn.Linear(num_hiddens, vocab_size)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_state</span>(<span class="params">self, enc_outputs, *args</span>):</span><br><span class="line">        <span class="keyword">return</span> enc_outputs[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># X = Y(batch_size, seq_len), 训练数据</span></span><br><span class="line">    <span class="comment"># state(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, state</span>):</span><br><span class="line">        X = <span class="variable language_">self</span>.embedding(X).permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        <span class="comment"># X(seq_len, batch_size, input_size)</span></span><br><span class="line">        <span class="comment"># seq_len， 是训练集的标签Y的序列长度</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 编码器的上下文变量context：(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">        <span class="comment"># state的形状:(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">        context = state[-<span class="number">1</span>].repeat(X.shape[<span class="number">0</span>], <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        <span class="comment"># context (num_steps（目标序列长度）, batch_size, num_hiddens)</span></span><br><span class="line">        <span class="comment"># X(num_steps（目标序列长度）, batch_size, input_size)</span></span><br><span class="line">        X_and_context = torch.cat((X, context), <span class="number">2</span>) <span class="comment"># 输入Y + C</span></span><br><span class="line">        <span class="comment"># Y+X (num_steps（目标序列长度）, batch_size, input_size + num_hiddens)</span></span><br><span class="line">        <span class="comment">#                                            就是embed_size + num_hiddens</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        output, state = <span class="variable language_">self</span>.rnn(X_and_context, state)</span><br><span class="line">        output = <span class="variable language_">self</span>.dense(output).permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        <span class="comment"># output的形状:(batch_size,num_steps,vocab_size)</span></span><br><span class="line">        <span class="comment"># state的形状:(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">        <span class="keyword">return</span> output, state</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">decoder = Seq2SeqDecoder(vocab_size=<span class="number">10</span>, embed_size=<span class="number">8</span>, num_hiddens=<span class="number">16</span>,</span><br><span class="line">                         num_layers=<span class="number">2</span>)</span><br><span class="line">decoder.<span class="built_in">eval</span>()</span><br><span class="line">state = decoder.init_state(encoder(X))</span><br><span class="line">output, state = decoder(X, state) <span class="comment"># X= (batch_size, seq_len)，元素为ID，但是含义不一样</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(output.shape)</span><br><span class="line"><span class="built_in">print</span>(state.shape)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>注意，这里虽然还是用X（4，7）作为输入，但是意义完全变了，这里的X其实是Y，也就是<strong>训练集的标签Y</strong></p><p>当我们在线测试的时候，一个时间步，喂入上一个时间步的输出，就相当于一次性给入整个训练集标签了</p><h4 id="损失函数"><a href="#损失函数" class="headerlink" title="损失函数"></a>损失函数</h4><p>在每个时间步，<strong>解码器预测了输出词元的概率分布</strong></p><p>类似于<strong>语言模型</strong>，可以使用<strong>softmax来获得分布</strong>，并通过<strong>计算交叉熵损失函数</strong>来进行优化</p><p>注意：我们之前用填充词添加到序列里面，这样能够让不同长度的序列，能够相同形状进行加载，所以我们在损失计算的时候，需要排除掉这些填充词</p><blockquote><p>模型架构（Encoder-Decoder）确实支持不同长度，但为了<strong>计算效率</strong>，我们必须在“<strong>批处理</strong>（Batching）”这一层做填充。<br><img src="/images/Pasted%20image%2020260507171456.png" alt="358"><br><img src="/images/Pasted%20image%2020260507171512.png" alt="478"></p></blockquote><p>所以，我们还是要对不同长度的序列，做填充，这样才能把不同长度的序列，塞到一个X（seq_len, batch_size, input_size）里面，让self.rnn， 进行批量化计算，一次计算一个batch的样本。</p><p>将<strong>填充词元的预测排除在损失函数的计算之外</strong>，在代码实现上，我们通常使用 <strong>掩码（Masking）</strong> 技术</p><p><strong>准备一个 Mask</strong>：对于一个带有填充的序列，我们生成一个同维度的布尔矩阵，有效词元处为 <code>1</code>，填充处为 <code>0</code>。（seq_len, batch_size, input_size）</p><p><strong>过滤 Loss</strong>：在计算交叉熵损失时，只保留 Mask 为 <code>1</code> 的部分的损失，将 Mask 为 <code>0</code> 的部分强制置为 <code>0</code>。</p><p>在训练过程中，由于我们采用了“方方正正”的矩阵输入，模型的<strong>输出也确实是“方方正正”的</strong>。</p><p><img src="/images/Pasted%20image%2020260507171929.png" alt="450"></p><p><strong>例如，如果两个序列的有效长度（不包括填充词元）分别为1和2， 则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零</strong></p><p>现在，我们可以通过<strong>扩展softmax交叉熵损失函数</strong>来<strong>遮蔽不相关的预测</strong>, 自己实现一个带遮蔽功能的softmax交叉熵损失函数</p><h4 id="训练"><a href="#训练" class="headerlink" title="训练"></a>训练</h4><p>训练，输入的一个样本（X，Y）包含两个序列，<br>我们需要先把X输入到编码器中，然后以他的状态为基础，</p><p>这个时候，整个序列-序列模型，其实就已经是一个单纯的解码器的循环神经网络了。</p><p>我们循环神经网络的训练中，是<br>（abcde）-&gt;(bcdef), 通过把后一个token作为该输入token的标签来的，这就是语言模型</p><p><strong>语言模型的训练样本，就是一个序列</strong>，<strong>把下一个token作为当前token输入的预测的标签</strong>，</p><p>一旦编码器（Encoder）完成了任务，接下来的训练过程在<strong>本质上就是一个带条件的语言模型（Conditional Language Model）训练</strong>。<br><img src="/images/Pasted%20image%2020260507214532.png" alt="633"></p><p>“把下一个 token 作为当前 token 输入的预测标签”正是 <strong>Teacher Forcing（强制教学）</strong> 的精髓。<br><img src="/images/Pasted%20image%2020260507214607.png" alt="331"></p><p><strong>因为语言模型，他的样本就是一个序列，通常也只预测下一位，也就是我们的经典的自监督学习</strong></p><h4 id="预测"><a href="#预测" class="headerlink" title="预测"></a>预测</h4><p>所以，如果输入的状态被重置，那么解码器的循环神经网络GRU，就会认为是另一个batch的输入，</p><p>所以如果是随机采样，不同batch之间不应该有状态的相关，所以，如果我们自己实现循环神经网络，就需要自己手动不同batch输入时，清除状态。</p><p>如果是顺序采样，那么前后两个batch的状态是需要继承的。所以本质上这几个batch是一个大序列里面的连续的，</p><p>所以在预测的时候，我们使用(1,1)的输入，虽然每次把预测的输出当作输入自回归给模型作为输入，严格意义上来说自回归，是顺序分区下的不同batch，这些个不同batch的样本序列，共用一个状态</p><p>我们训练的时候则是随机采样下的，一个状态，只在同一个batch来训练的。</p><p><img src="/images/Pasted%20image%2020260507213919.png"></p><p><img src="/images/Pasted%20image%2020260507213953.png" alt="629"></p><p><img src="/images/Pasted%20image%2020260507214016.png" alt="626"></p><h4 id="完整实现（非常有学习价值）"><a href="#完整实现（非常有学习价值）" class="headerlink" title="完整实现（非常有学习价值）"></a>完整实现（非常有学习价值）</h4><p>数据集准备脚本<br><code>translate.py</code></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line">d2l.DATA_HUB[<span class="string">&#x27;fra-eng&#x27;</span>] = (d2l.DATA_URL + <span class="string">&#x27;fra-eng.zip&#x27;</span>, <span class="string">&#x27;94646ad1522d915e7b0f9296181140edcf86a4f5&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">read_data_nmt</span>():</span><br><span class="line">    data_dir = d2l.download_extract(<span class="string">&#x27;fra-eng&#x27;</span>)</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(os.path.join(data_dir, <span class="string">&#x27;fra.txt&#x27;</span>), <span class="string">&#x27;r&#x27;</span>, encoding=<span class="string">&#x27;utf-8&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">        <span class="keyword">return</span> f.read()</span><br><span class="line"></span><br><span class="line">raw_text = read_data_nmt()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">preprocess_nmt</span>(<span class="params">text</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">no_space</span>(<span class="params">char, prev_char</span>):</span><br><span class="line">        <span class="keyword">return</span> char <span class="keyword">in</span> <span class="built_in">set</span>(<span class="string">&#x27;,.!?&#x27;</span>) <span class="keyword">and</span> prev_char != <span class="string">&#x27; &#x27;</span></span><br><span class="line">    </span><br><span class="line">    text= text.replace(<span class="string">&#x27;\u202f&#x27;</span>, <span class="string">&#x27; &#x27;</span>).replace(<span class="string">&#x27;\xa0&#x27;</span>, <span class="string">&#x27; &#x27;</span>).lower()</span><br><span class="line">    out = [<span class="string">&#x27; &#x27;</span> + char <span class="keyword">if</span> i &gt; <span class="number">0</span> <span class="keyword">and</span> no_space(char, text[i-<span class="number">1</span>]) <span class="keyword">else</span> char <span class="keyword">for</span> i, char <span class="keyword">in</span> <span class="built_in">enumerate</span>(text)]</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;&#x27;</span>.join(out)</span><br><span class="line"></span><br><span class="line">text = preprocess_nmt(raw_text)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">tokenize_nmt</span>(<span class="params">text, num_examples=<span class="literal">None</span></span>):</span><br><span class="line">    source = []</span><br><span class="line">    target = []</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i, line) <span class="keyword">in</span> <span class="built_in">enumerate</span>(text.split(<span class="string">&#x27;\n&#x27;</span>)):</span><br><span class="line">        <span class="comment"># 是否超出要读取的样本数</span></span><br><span class="line">        <span class="keyword">if</span> num_examples <span class="keyword">and</span> i &gt; num_examples:</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">        parts = line.split(<span class="string">&#x27;\t&#x27;</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(parts) == <span class="number">2</span>:</span><br><span class="line">            source.append(parts[<span class="number">0</span>].split(<span class="string">&#x27; &#x27;</span>))</span><br><span class="line">            target.append(parts[<span class="number">1</span>].split(<span class="string">&#x27; &#x27;</span>))</span><br><span class="line">    <span class="keyword">return</span> source, target</span><br><span class="line"></span><br><span class="line">source, target = tokenize_nmt(text)</span><br><span class="line"><span class="built_in">print</span>(source[:<span class="number">6</span>])</span><br><span class="line"><span class="built_in">print</span>(target[:<span class="number">6</span>])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">src_vocab = d2l.Vocab(source, min_freq=<span class="number">2</span>, reserved_tokens=[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>, <span class="string">&#x27;&lt;bos&gt;&#x27;</span>, <span class="string">&#x27;&lt;eos&gt;&#x27;</span>])</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(src_vocab))</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">type</span>(src_vocab))</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(src_vocab.token_to_idx))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">truncate_pad</span>(<span class="params">line, num_steps, padding_token</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(line) &gt; num_steps:</span><br><span class="line">        <span class="keyword">return</span> line[:num_steps]</span><br><span class="line">    <span class="keyword">return</span> line + [padding_token]*(num_steps - <span class="built_in">len</span>(line))</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(truncate_pad(src_vocab[source[<span class="number">0</span>]], <span class="number">5</span>, src_vocab[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>]))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">build_array_nmt</span>(<span class="params">lines, vocab, num_steps</span>):</span><br><span class="line">    lines = [vocab[l] <span class="keyword">for</span> l <span class="keyword">in</span> lines]</span><br><span class="line">    lines = [l + [vocab[<span class="string">&#x27;&lt;eos&gt;&#x27;</span>]] <span class="keyword">for</span> l <span class="keyword">in</span> lines]</span><br><span class="line">    array = torch.tensor([truncate_pad(line=l,num_steps=num_steps, padding_token=vocab[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>]) <span class="keyword">for</span> l <span class="keyword">in</span> lines])</span><br><span class="line">    valid_len = (array != vocab[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>]).<span class="built_in">type</span>(torch.int32).<span class="built_in">sum</span>(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">return</span> array, valid_len</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">load_data_nmt</span>(<span class="params">batch_size, num_steps, num_examples = <span class="number">600</span></span>):</span><br><span class="line">    text = preprocess_nmt(read_data_nmt())</span><br><span class="line">    source, target = tokenize_nmt(text, num_examples)</span><br><span class="line">    src_vocab = d2l.Vocab(source, min_freq=<span class="number">2</span>,</span><br><span class="line">                          reserved_tokens=[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>, <span class="string">&#x27;&lt;bos&gt;&#x27;</span>, <span class="string">&#x27;&lt;eos&gt;&#x27;</span>])</span><br><span class="line">    tgt_vocab = d2l.Vocab(target, min_freq=<span class="number">2</span>,</span><br><span class="line">                          reserved_tokens=[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>, <span class="string">&#x27;&lt;bos&gt;&#x27;</span>, <span class="string">&#x27;&lt;eos&gt;&#x27;</span>])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)</span><br><span class="line">    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)</span><br><span class="line"></span><br><span class="line">    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)</span><br><span class="line">    data_iter = d2l.load_array(data_arrays, batch_size)</span><br><span class="line">    <span class="keyword">return</span> data_iter, src_vocab, tgt_vocab</span><br><span class="line"></span><br><span class="line">train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=<span class="number">2</span>, num_steps=<span class="number">8</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> X, X_valid_len, Y, Y_valid_len <span class="keyword">in</span> train_iter:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;X:&#x27;</span>, X.<span class="built_in">type</span>(torch.int32))</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;X的有效长度:&#x27;</span>, X_valid_len)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;Y:&#x27;</span>, Y.<span class="built_in">type</span>(torch.int32))</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;Y的有效长度:&#x27;</span>, Y_valid_len)</span><br><span class="line">    <span class="keyword">break</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>encoder_decoder.py</code></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> collections</span><br><span class="line"><span class="keyword">import</span> math</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"><span class="keyword">import</span> translate</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"> </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Encoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基本编码器接口&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Encoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, *args</span>):</span><br><span class="line">        <span class="keyword">raise</span> NotImplementedError</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Decoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基本解码器接口&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Decoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_state</span>(<span class="params">self, enc_outputs, *args</span>):</span><br><span class="line">        <span class="keyword">raise</span> NotImplementedError</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, state</span>):</span><br><span class="line">        <span class="keyword">raise</span> NotImplementedError</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">EncoderDecoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基类&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, encoder, decoder, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(EncoderDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.encoder = encoder</span><br><span class="line">        <span class="variable language_">self</span>.decoder = decoder</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, enc_X, dec_X, *args</span>):</span><br><span class="line">        enc_outputs = <span class="variable language_">self</span>.encoder(enc_X, *args)</span><br><span class="line">        dec_state = <span class="variable language_">self</span>.decoder.init_state(enc_outputs, *args)</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.decoder(dec_X, dec_state)</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 class="comment">#@save</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">嵌入层+2层GRU</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Seq2SeqEncoder</span>(<span class="title class_ inherited__">Encoder</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;用于序列到序列学习的循环神经网络编码器</span></span><br><span class="line"><span class="string">    vocab_size: token种类</span></span><br><span class="line"><span class="string">    embed_size: input_size</span></span><br><span class="line"><span class="string">    num_hiddens: hidden_size</span></span><br><span class="line"><span class="string">    num_layers: 隐藏层层数</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embed_size, num_hiddens, num_layers,</span></span><br><span class="line"><span class="params">                 dropout=<span class="number">0</span>, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Seq2SeqEncoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="comment"># 嵌入层(embed_size = 特征向量维度 = input_size)</span></span><br><span class="line">        <span class="variable language_">self</span>.embedding = nn.Embedding(vocab_size, embed_size)</span><br><span class="line">        <span class="variable language_">self</span>.rnn = nn.GRU(embed_size, num_hiddens, num_layers,</span><br><span class="line">                          dropout=dropout)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># X,一个batch的所有token: (batch_size, seq_len)，每个元素是ID</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, *args</span>):</span><br><span class="line">        X = <span class="variable language_">self</span>.embedding(X)</span><br><span class="line">        <span class="comment"># 输出&#x27;X&#x27;的形状：(batch_size, seq_len, embed_size)</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 交换轴0，轴1,(seq_len, batch_size, embed_size)</span></span><br><span class="line">        X = X.permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 如果未提及状态，则默认为0</span></span><br><span class="line">        output, state = <span class="variable language_">self</span>.rnn(X)</span><br><span class="line">        <span class="comment"># output的形状:(seq_len, batch_size, hidden_size)</span></span><br><span class="line">        <span class="comment"># state的形状:(num_layers,batch_size, hidden_size)， 两层隐藏层，最后一个时间步的状态</span></span><br><span class="line">        <span class="keyword">return</span> output, state</span><br><span class="line">        <span class="comment"># 这个state, 返回的是所有隐藏层的最后一个时间步的状态</span></span><br><span class="line">    </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">编码器循环神经网络：</span></span><br><span class="line"><span class="string">10种token, </span></span><br><span class="line"><span class="string">input_size=8, </span></span><br><span class="line"><span class="string">hiddens_size=16,</span></span><br><span class="line"><span class="string">隐藏层2层</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">encoder = Seq2SeqEncoder(vocab_size=<span class="number">10</span>, embed_size=<span class="number">8</span>, num_hiddens=<span class="number">16</span>,</span><br><span class="line">                         num_layers=<span class="number">2</span>)</span><br><span class="line">encoder.<span class="built_in">eval</span>() <span class="comment"># 设置该隐藏层可以修改参数</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># X= (batch_size, seq_len)，元素为ID</span></span><br><span class="line">X = torch.zeros((<span class="number">4</span>, <span class="number">7</span>), dtype=torch.long) </span><br><span class="line"></span><br><span class="line"><span class="comment"># output的形状:(seq_len, batch_size, hidden_size)</span></span><br><span class="line"><span class="comment"># state的形状:(num_layers,batch_size, hidden_size)， 两层隐藏层，最后一个时间步的状态</span></span><br><span class="line">output, state = encoder(X)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(output.shape)</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"></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 class="keyword">class</span> <span class="title class_">Seq2SeqDecoder</span>(<span class="title class_ inherited__">Decoder</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;用于序列到序列学习的循环神经网络解码器</span></span><br><span class="line"><span class="string">    vocab_size: token种类</span></span><br><span class="line"><span class="string">    embed_size: input_size</span></span><br><span class="line"><span class="string">    num_hiddens: hidden_size</span></span><br><span class="line"><span class="string">    num_layers: 解码器隐藏层层数</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embed_size, num_hiddens, num_layers,</span></span><br><span class="line"><span class="params">                 dropout=<span class="number">0</span>, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Seq2SeqDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        注意，我们训练的时候，需要批量化输入，但是在实际测试的时候，不需要</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        <span class="comment"># 解码器也有嵌入层，用来预处理数据，输入(batch_size, seq_len)</span></span><br><span class="line">        <span class="variable language_">self</span>.embedding = nn.Embedding(vocab_size, embed_size)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 和编码器一样的两层隐藏层，输入是（Y-embed_size + C-num_hiddens）</span></span><br><span class="line">        <span class="variable language_">self</span>.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,</span><br><span class="line">                          dropout=dropout)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 输出层，MLP，输出的是token种类的打分</span></span><br><span class="line">        <span class="variable language_">self</span>.dense = nn.Linear(num_hiddens, vocab_size)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 处理编码器得到的context C</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_state</span>(<span class="params">self, enc_outputs, *args</span>):</span><br><span class="line">        <span class="keyword">return</span> enc_outputs[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    解码器输入：yt-1 + C(上下文变量)</span></span><br><span class="line"><span class="string">         输出: 预测yt</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    数据集：（X，Y）， X(英语)给编码器作为输入，Y(法语)给解码器作为标签</span></span><br><span class="line"><span class="string">    就像你训练RNN一样，你肯定是把序列逐个输入，然后和标签比较，不是一直自回归。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    训练预测下一个</span></span><br><span class="line"><span class="string">    (abcde)-(bcdef), 你肯定是</span></span><br><span class="line"><span class="string">        输入a，得到y1_hat, 和y1比较得到loss</span></span><br><span class="line"><span class="string">        输入b, 得到y2_hat, 和y2比较得到loss</span></span><br><span class="line"><span class="string">        ...</span></span><br><span class="line"><span class="string">        输入d,得到yt_hat, 和yt比较得到loss</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    测试：</span></span><br><span class="line"><span class="string">    输入a, 预测b, 输入预测b, 预测c, ....</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># X = Y(batch_size, seq_len_y)</span></span><br><span class="line">    <span class="comment"># 编码器的隐藏层状态：state(num_layers, batch_size, hidden_size)</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, state</span>):</span><br><span class="line"></span><br><span class="line">        X = <span class="variable language_">self</span>.embedding(X).permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        <span class="comment"># X(seq_len_y, batch_size, input_size). seq_len_y, 是训练集的标签Y的序列长度</span></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 class="comment"># state的形状:(num_layers, batch_size, hidden_size)</span></span><br><span class="line">        <span class="comment"># state[-1], 第二层隐藏层的最后一个Ht (batch_size, hidden_size)</span></span><br><span class="line">        <span class="comment"># X.shape[0] = seq_len_y</span></span><br><span class="line">        context = state[-<span class="number">1</span>].repeat(X.shape[<span class="number">0</span>], <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        <span class="comment"># 编码器的上下文变量context：(seq_len_y, batch_size, hidden_size)</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        为什么仅选择最后一层的Ht作为上下文变量？</span></span><br><span class="line"><span class="string">        更高层代表更高级的特征</span></span><br><span class="line"><span class="string">        - 编码器的所有隐藏层的最后时间步的状态，已经作为解码器的最初状态了</span></span><br><span class="line"><span class="string">        - 用更高的特征Context， 来作为补充输入，作为额外强化记忆</span></span><br><span class="line"><span class="string">        - 解码器本身的输入维度已经固定了（input_size + hidden_size）</span></span><br><span class="line"><span class="string">        - 如果所有层的最后状态，作为context, 就会变成（input_size + num_layers * hidden_size）</span></span><br><span class="line"><span class="string">          会导致信息冗余，增加训练难度</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 编码器的隐藏层的输入张量</span></span><br><span class="line">        X_and_context = torch.cat((X, context), <span class="number">2</span>) <span class="comment"># 输入Y + C</span></span><br><span class="line">        <span class="comment"># X_and_context = Y+X (seq_len_y, batch_size,  input_size + num_hiddens)</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="comment"># state: 用编码器的最后一步的隐状态HT 去初始化 解码器的初始状态H0&#x27;</span></span><br><span class="line">        <span class="comment"># X_and_context: Yt-1 + C</span></span><br><span class="line">        output, state = <span class="variable language_">self</span>.rnn(X_and_context, state)</span><br><span class="line">        <span class="comment"># 经过隐藏层之后，</span></span><br><span class="line">        <span class="comment"># output: (seq_len_y, batch_size, hidden_size)</span></span><br><span class="line">        <span class="comment"># state: (num_layers, batch_size, hidden_size)</span></span><br><span class="line"></span><br><span class="line">        </span><br><span class="line">        output = <span class="variable language_">self</span>.dense(output).permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        <span class="comment"># output的形状:(batch_size, seq_len_y, vocab_size)</span></span><br><span class="line">        <span class="comment"># state的形状:(num_layers,batch_size,num_hiddens)</span></span><br><span class="line">        <span class="keyword">return</span> output, state</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">解码器：</span></span><br><span class="line"><span class="string">token种类10</span></span><br><span class="line"><span class="string">input_size = 8</span></span><br><span class="line"><span class="string">hidden_size = 16</span></span><br><span class="line"><span class="string">隐藏层2层</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">decoder = Seq2SeqDecoder(vocab_size=<span class="number">10</span>, embed_size=<span class="number">8</span>, num_hiddens=<span class="number">16</span>,</span><br><span class="line">                         num_layers=<span class="number">2</span>)</span><br><span class="line">decoder.<span class="built_in">eval</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">#数据集（X，X）</span></span><br><span class="line"><span class="comment"># X (batch_size, seq_len_y)</span></span><br><span class="line"><span class="comment"># state:(num_layers,batch_size, hidden_size)</span></span><br><span class="line">state = decoder.init_state(encoder(X))</span><br><span class="line"></span><br><span class="line"><span class="comment"># X=Y (batch_size, seq_len_y)</span></span><br><span class="line"><span class="comment"># state:(num_layers,batch_size, hidden_size): 解码器的最后一个时间步的所有隐藏层的状态</span></span><br><span class="line">output, state = decoder(X, state)</span><br><span class="line"><span class="comment"># output的形状:(batch_size, seq_len_y, vocab_size)</span></span><br><span class="line"><span class="comment"># state的形状:(num_layers,batch_size,num_hiddens)</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(output.shape) <span class="comment"># torch.Size([4, 7, 10])</span></span><br><span class="line"><span class="built_in">print</span>(state.shape) <span class="comment"># torch.Size([2, 4, 16])</span></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"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">损失函数</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="comment"># 保留每个序列样本的指定个数</span></span><br><span class="line"><span class="comment"># X (batch_size, seq_len) 原始batch序列</span></span><br><span class="line"><span class="comment"># valid_len (len, ) 用来指定每个样本的有效长度</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">sequence_mask</span>(<span class="params">X, valid_len, value=<span class="number">0</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;在序列中屏蔽不相关的项&quot;&quot;&quot;</span></span><br><span class="line">    maxlen = X.size(<span class="number">1</span>)</span><br><span class="line">    mask = torch.arange((maxlen), dtype=torch.float32,</span><br><span class="line">                        device=X.device)[<span class="literal">None</span>, :] &lt; valid_len[:, <span class="literal">None</span>]</span><br><span class="line">    X[~mask] = value</span><br><span class="line">    <span class="keyword">return</span> X</span><br><span class="line"></span><br><span class="line">X = torch.tensor([[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>], [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>]])</span><br><span class="line"><span class="built_in">print</span>(sequence_mask(X, torch.tensor([<span class="number">1</span>, <span class="number">2</span>])))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">带掩码的交叉熵损失函数</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MaskedSoftmaxCELoss</span>(nn.CrossEntropyLoss):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;带遮蔽的softmax交叉熵损失函数&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># pred的形状：(batch_size, num_steps, vocab_size（token种类的打分）)</span></span><br><span class="line">    <span class="comment"># label的形状：(batch_size,num_steps)</span></span><br><span class="line">    <span class="comment"># valid_len的形状：(batch_size,)</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, pred, label, valid_len</span>):</span><br><span class="line">        weights = torch.ones_like(label)</span><br><span class="line">        weights = sequence_mask(weights, valid_len)</span><br><span class="line"></span><br><span class="line">        <span class="variable language_">self</span>.reduction=<span class="string">&#x27;none&#x27;</span></span><br><span class="line">        unweighted_loss = <span class="built_in">super</span>(MaskedSoftmaxCELoss, <span class="variable language_">self</span>).forward(</span><br><span class="line">            pred.permute(<span class="number">0</span>, <span class="number">2</span>, <span class="number">1</span>), label)</span><br><span class="line">        weighted_loss = (unweighted_loss * weights).mean(dim=<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">return</span> weighted_loss</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"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">train_seq2seq</span>(<span class="params">net, data_iter, lr, num_epochs, tgt_vocab, device</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;训练序列到序列模型&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">xavier_init_weights</span>(<span class="params">m</span>):</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.Linear:</span><br><span class="line">            nn.init.xavier_uniform_(m.weight)</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">type</span>(m) == nn.GRU:</span><br><span class="line">            <span class="keyword">for</span> param <span class="keyword">in</span> m._flat_weights_names:</span><br><span class="line">                <span class="keyword">if</span> <span class="string">&quot;weight&quot;</span> <span class="keyword">in</span> param:</span><br><span class="line">                    nn.init.xavier_uniform_(m._parameters[param])</span><br><span class="line"></span><br><span class="line">    net.apply(xavier_init_weights)</span><br><span class="line">    net.to(device)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 优化器：Adam</span></span><br><span class="line">    optimizer = torch.optim.Adam(net.parameters(), lr=lr)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 损失函数： 带掩码的交叉熵损失函数</span></span><br><span class="line">    loss = MaskedSoftmaxCELoss()</span><br><span class="line">    </span><br><span class="line">    net.train()</span><br><span class="line">    animator = d2l.Animator(xlabel=<span class="string">&#x27;epoch&#x27;</span>, ylabel=<span class="string">&#x27;loss&#x27;</span>,</span><br><span class="line">                     xlim=[<span class="number">10</span>, num_epochs])</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(num_epochs):</span><br><span class="line">        timer = d2l.Timer()</span><br><span class="line">        metric = d2l.Accumulator(<span class="number">2</span>)  <span class="comment"># 训练损失总和，词元数量</span></span><br><span class="line">        <span class="comment"># batch = (X, X_len, Y, Y_len), X(batch_size, seq_len_x), Y(batch_size, seq_len_y)</span></span><br><span class="line">        <span class="keyword">for</span> batch <span class="keyword">in</span> data_iter:</span><br><span class="line">            optimizer.zero_grad()</span><br><span class="line">            X, X_valid_len, Y, Y_valid_len = [x.to(device) <span class="keyword">for</span> x <span class="keyword">in</span> batch]</span><br><span class="line">            bos = torch.tensor([tgt_vocab[<span class="string">&#x27;&lt;bos&gt;&#x27;</span>]] * Y.shape[<span class="number">0</span>],</span><br><span class="line">                          device=device).reshape(-<span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">            dec_input = torch.cat([bos, Y[:, :-<span class="number">1</span>]], <span class="number">1</span>)  <span class="comment"># 强制教学</span></span><br><span class="line">            Y_hat, _ = net(X, dec_input, X_valid_len)</span><br><span class="line">            <span class="comment">#print(&quot;check:&quot;,Y_hat.shape)</span></span><br><span class="line">            l = loss(Y_hat, Y, Y_valid_len)</span><br><span class="line">            l.<span class="built_in">sum</span>().backward()      <span class="comment"># 损失函数的标量进行“反向传播”</span></span><br><span class="line">            d2l.grad_clipping(net, <span class="number">1</span>)</span><br><span class="line">            num_tokens = Y_valid_len.<span class="built_in">sum</span>()</span><br><span class="line">            optimizer.step()</span><br><span class="line">            <span class="keyword">with</span> torch.no_grad():</span><br><span class="line">                metric.add(l.<span class="built_in">sum</span>(), num_tokens)</span><br><span class="line">        <span class="keyword">if</span> (epoch + <span class="number">1</span>) % <span class="number">10</span> == <span class="number">0</span>:</span><br><span class="line">            animator.add(epoch + <span class="number">1</span>, (metric[<span class="number">0</span>] / metric[<span class="number">1</span>],))</span><br><span class="line">        </span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;epoch = &quot;</span>, epoch, <span class="string">&quot; loss = &quot;</span>, metric[<span class="number">0</span>] / metric[<span class="number">1</span>])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&#x27;loss <span class="subst">&#123;metric[<span class="number">0</span>] / metric[<span class="number">1</span>]:<span class="number">.3</span>f&#125;</span>, <span class="subst">&#123;metric[<span class="number">1</span>] / timer.stop():<span class="number">.1</span>f&#125;</span> &#x27;</span></span><br><span class="line">        <span class="string">f&#x27;tokens/sec on <span class="subst">&#123;<span class="built_in">str</span>(device)&#125;</span>&#x27;</span>)</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 class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">预测</span></span><br><span class="line"><span class="string">src_sentence 为一个样本(X,Y)的X序列(seq_len,)</span></span><br><span class="line"><span class="string">src_vocab 为源序列的词表</span></span><br><span class="line"><span class="string">tgt_vocab 为目标序列 词表</span></span><br><span class="line"><span class="string">num_steps 规定的填充序列长度</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="comment">#@save</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">predict_seq2seq</span>(<span class="params">net, src_sentence, src_vocab, tgt_vocab, num_steps,</span></span><br><span class="line"><span class="params">                    device, save_attention_weights=<span class="literal">False</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;序列到序列模型的预测&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 在预测时将net设置为评估模式</span></span><br><span class="line">    net.<span class="built_in">eval</span>()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 该样本源序列，字符串转token list</span></span><br><span class="line">    src_tokens = src_vocab[src_sentence.lower().split(<span class="string">&#x27; &#x27;</span>)] + [</span><br><span class="line">        src_vocab[<span class="string">&#x27;&lt;eos&gt;&#x27;</span>]]</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># enc_valid_len 为该样本的源序列的有效长度（不包含填充词的长度）</span></span><br><span class="line">    enc_valid_len = torch.tensor([<span class="built_in">len</span>(src_tokens)], device=device)</span><br><span class="line">    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab[<span class="string">&#x27;&lt;pad&gt;&#x27;</span>])</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 编码器推理</span></span><br><span class="line">    <span class="comment"># 把一个序列，构造成编码器的输入：</span></span><br><span class="line">    <span class="comment"># enc_X : ( 1, seq_len=num_steps) )</span></span><br><span class="line">    enc_X = torch.unsqueeze(</span><br><span class="line">        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=<span class="number">0</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 这里的enc_valid_len似乎没有用到</span></span><br><span class="line">    enc_outputs = net.encoder(enc_X, enc_valid_len)</span><br><span class="line">    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 解码器自回归</span></span><br><span class="line">    <span class="comment"># 构造一个只含有&quot;bos&quot;开始位的输入的（1, 1 = seq_len）, 用来启动自回归</span></span><br><span class="line">    dec_X = torch.unsqueeze(torch.tensor(</span><br><span class="line">        [tgt_vocab[<span class="string">&#x27;&lt;bos&gt;&#x27;</span>]], dtype=torch.long, device=device), dim=<span class="number">0</span>)</span><br><span class="line">    </span><br><span class="line">    output_seq, attention_weight_seq = [], []</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 我们要预测完一个完整的填充固定长度的序列，除非预测出结束eos</span></span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(num_steps):</span><br><span class="line">        <span class="comment"># Y:(batch_size, seq_len_y, vocab_size) （1，1，vocab_size）</span></span><br><span class="line">        <span class="comment"># dec_state:(num_layers,batch_size,num_hiddens) (2,1,hidden_size)</span></span><br><span class="line">        Y, dec_state = net.decoder(dec_X, dec_state)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 我们使用具有预测最高可能性的词元，作为解码器在下一时间步的输入</span></span><br><span class="line">        <span class="comment"># 选择token种类的打分最多的一个</span></span><br><span class="line">        <span class="comment"># dec_X:(1 , 1) (batch_size, seq_len_y), 此时dec_X就是ID，预测出来的ID</span></span><br><span class="line">        dec_X = Y.argmax(dim=<span class="number">2</span>) </span><br><span class="line"></span><br><span class="line">        <span class="comment"># dec_X (1,1) 转成 int32 ID </span></span><br><span class="line">        pred = dec_X.squeeze(dim=<span class="number">0</span>).<span class="built_in">type</span>(torch.int32).item()</span><br><span class="line">        <span class="comment"># 保存注意力权重（稍后讨论）</span></span><br><span class="line">        <span class="keyword">if</span> save_attention_weights:</span><br><span class="line">            attention_weight_seq.append(net.decoder.attention_weights)</span><br><span class="line">        <span class="comment"># 一旦序列结束词元被预测，输出序列的生成就完成了</span></span><br><span class="line">        <span class="keyword">if</span> pred == tgt_vocab[<span class="string">&#x27;&lt;eos&gt;&#x27;</span>]:</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 加入到自己预测的输出序列里面</span></span><br><span class="line">        output_seq.append(pred)</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27; &#x27;</span>.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq</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 class="keyword">def</span> <span class="title function_">bleu</span>(<span class="params">pred_seq, label_seq, k</span>):  <span class="comment">#@save</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;计算BLEU&quot;&quot;&quot;</span></span><br><span class="line">    pred_tokens, label_tokens = pred_seq.split(<span class="string">&#x27; &#x27;</span>), label_seq.split(<span class="string">&#x27; &#x27;</span>)</span><br><span class="line">    len_pred, len_label = <span class="built_in">len</span>(pred_tokens), <span class="built_in">len</span>(label_tokens)</span><br><span class="line">    score = math.exp(<span class="built_in">min</span>(<span class="number">0</span>, <span class="number">1</span> - len_label / len_pred))</span><br><span class="line">    <span class="keyword">for</span> n <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, k + <span class="number">1</span>):</span><br><span class="line">        num_matches, label_subs = <span class="number">0</span>, collections.defaultdict(<span class="built_in">int</span>)</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(len_label - n + <span class="number">1</span>):</span><br><span class="line">            label_subs[<span class="string">&#x27; &#x27;</span>.join(label_tokens[i: i + n])] += <span class="number">1</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(len_pred - n + <span class="number">1</span>):</span><br><span class="line">            <span class="keyword">if</span> label_subs[<span class="string">&#x27; &#x27;</span>.join(pred_tokens[i: i + n])] &gt; <span class="number">0</span>:</span><br><span class="line">                num_matches += <span class="number">1</span></span><br><span class="line">                label_subs[<span class="string">&#x27; &#x27;</span>.join(pred_tokens[i: i + n])] -= <span class="number">1</span></span><br><span class="line">        score *= math.<span class="built_in">pow</span>(num_matches / (len_pred - n + <span class="number">1</span>), math.<span class="built_in">pow</span>(<span class="number">0.5</span>, n))</span><br><span class="line">    <span class="keyword">return</span> score</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"></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"></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"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">input_size = 32</span></span><br><span class="line"><span class="string">hidden_size = 32</span></span><br><span class="line"><span class="string">隐藏层： 2层</span></span><br><span class="line"><span class="string">dropout丢弃率 0.1</span></span><br><span class="line"><span class="string">batch_szie= 64</span></span><br><span class="line"><span class="string">seq_len = 10</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">embed_size, num_hiddens, num_layers, dropout = <span class="number">32</span>, <span class="number">32</span>, <span class="number">2</span>, <span class="number">0.1</span></span><br><span class="line">batch_size, num_steps = <span class="number">64</span>, <span class="number">10</span></span><br><span class="line">lr, num_epochs, device = <span class="number">0.005</span>, <span class="number">200</span>, torch.device(<span class="string">&quot;cuda&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">train_iter, src_vocab, tgt_vocab = translate.train_iter, translate.src_vocab, translate.tgt_vocab</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">encoder = Seq2SeqEncoder(<span class="built_in">len</span>(src_vocab), embed_size, num_hiddens, num_layers,</span><br><span class="line">                        dropout)</span><br><span class="line">decoder = Seq2SeqDecoder(<span class="built_in">len</span>(tgt_vocab), embed_size, num_hiddens, num_layers,</span><br><span class="line">                        dropout)</span><br><span class="line">net = EncoderDecoder(encoder, decoder)</span><br><span class="line">train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">engs = [<span class="string">&#x27;go .&#x27;</span>, <span class="string">&quot;i lost .&quot;</span>, <span class="string">&#x27;he\&#x27;s calm .&#x27;</span>, <span class="string">&#x27;i\&#x27;m home .&#x27;</span>]</span><br><span class="line">fras = [<span class="string">&#x27;va !&#x27;</span>, <span class="string">&#x27;j\&#x27;ai perdu .&#x27;</span>, <span class="string">&#x27;il est calme .&#x27;</span>, <span class="string">&#x27;je suis chez moi .&#x27;</span>]</span><br><span class="line"><span class="keyword">for</span> eng, fra <span class="keyword">in</span> <span class="built_in">zip</span>(engs, fras):</span><br><span class="line">    <span class="comment"># eng 为一个样本(X,Y)的X序列(seq_len,)</span></span><br><span class="line">    <span class="comment"># src_vocab 为源序列的词表</span></span><br><span class="line">    <span class="comment"># tgt_vocab 为目标序列 词表</span></span><br><span class="line">    <span class="comment"># 规定的填充序列长度</span></span><br><span class="line">    translation, attention_weight_seq = predict_seq2seq(</span><br><span class="line">        net, eng, src_vocab, tgt_vocab, num_steps, device)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 输出自回归的结果，并和标签进行比较评估</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&#x27;<span class="subst">&#123;eng&#125;</span> =&gt; <span class="subst">&#123;translation&#125;</span>, bleu <span class="subst">&#123;bleu(translation, fra, k=<span class="number">2</span>):<span class="number">.3</span>f&#125;</span>&#x27;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="束搜索"><a href="#束搜索" class="headerlink" title="束搜索"></a>束搜索</h3><p>这里讲的搜索办法，是从模型得到的分布中得到一个确定的预测值的方法，也就是从最后一层的MLP的各个token种类的打分中，拿到一个确定的预测。</p><p>因为我们的目标是预测T‘个token， 所以我们的目的是为了让：<br><img src="/images/Pasted%20image%2020260508095414.png"></p><p>有以下几种方法：</p><ul><li><strong>贪心搜索</strong><ul><li><img src="/images/Pasted%20image%2020260508095456.png" alt="310"></li><li>每次都选则当前预测token分布的最大概率，也就是argmax那个操作</li><li><mark style="background:#fff88f">计算成本最低</mark></li><li>问题：<ul><li>这属于局部最优，并非全局最优，因为你前面的预测token选择的不一样，会导致下一个token预测的概率分布发生变化。</li></ul></li></ul></li><li>穷举搜索<ul><li>计算量太大，<mark style="background:#fff88f">精度最高</mark></li></ul></li><li>束搜索<ul><li>该策略，精度不是最高，成本也不是最低，介于上面两者之间</li><li><em>束搜索</em>（beam search）是<strong>贪心搜索的一个改进版本</strong></li><li>有一个<strong>超参数</strong>：<mark style="background:#fff88f">束宽k</mark><ul><li><p>第一个预测：y1, 我们选择<strong>最高条件概率</strong>的<strong>k个token</strong>， 这k个token是<strong>候选</strong>第一个token预测</p></li><li><p>第二个预测：y2, 基于前面的k个候选输出预测，继续分化前k个概率最大的。</p></li><li><p><img src="/images/Pasted%20image%2020260508100042.png" alt="535"></p></li><li><p><img src="/images/Pasted%20image%2020260508100211.png"></p></li><li><p><img src="/images/Pasted%20image%2020260508100315.png"></p></li></ul></li></ul></li></ul><p>那束搜索是如何具体实现的呢？</p><blockquote><p>在序列-序列的编码器解码器模型里面，这个束搜索，要如何来进行实际的运算预测呢？</p><p>我的理解是，最初，输入(1,1)&#x3D;(batch_size, seq_len)的token，也就是bos 来启动自回归，预测k个第一个时间步的预测token，然后这k个预测的token，作为一个新的batch来重新输入吗？</p><p>也就是第二个时间步，输入(k, 1) 的小批量，然后产生预测，是这样吗？</p><p>但是这样的话，内部的状态要怎么弄？我记得如果一开始输入（1，1)的小批量，batch_size &#x3D;1, 所以解码器的状态就是：（1， hidden_size）， 那这个时候，如果下一次输入的是（k， 1）， batch_size &#x3D; k， 这时候该怎么办呢？</p><p>所以束搜索是如何实现的呢？还是说，他把模型预测的进程复制了k份？</p></blockquote><p><img src="/images/Pasted%20image%2020260508100928.png" alt="541"></p><p><img src="/images/Pasted%20image%2020260508100956.png" alt="502"><br><img src="/images/Pasted%20image%2020260508101253.png" alt="532"></p><p><strong>第一层预测了k个，第二层理论上得到k^2个，我需要手动从这k^2个里面筛选出k个, 总之保证每次预测仅保留k个给后面，也就是束（等宽传递）</strong></p><p><img src="/images/Pasted%20image%2020260508101631.png" alt="618"></p><h3 id="补充：TCN"><a href="#补充：TCN" class="headerlink" title="补充：TCN"></a>补充：TCN</h3><p>TCN时序卷积神经网络，严格来说他不算RNN，但是他同样是处理序列输入的，借鉴了CNN和RNN，他把序列，看成是一个1xl的图片，通过一个1xk的卷积核，来做卷积运算</p><p><mark style="background:#affad1">卷积核的分类</mark><br><img src="/images/Pasted%20image%2020260510103600.png" alt="535"></p><ul><li>full卷积</li><li>same卷积</li><li>valid卷积</li></ul><p><mark style="background:#affad1">因果卷积</mark><br>这里的因果卷积，是说，我们每层卷积层（一个卷积核就是一个通道）的输出序列的每个token，只能向左一对多，不能用未来的输入序列的token来进行</p><p><img src="/images/Pasted%20image%2020260510104855.png" alt="599"></p><p><mark style="background:#affad1">膨胀卷积</mark><br>膨胀卷积的意思，就是，卷积层输出的每个token，原来是依靠原始输入序列的，和kernel_size相同的相邻序列来卷积计算，膨胀卷积，就是原始的输入序列，是采用的隔k隔选一个token，凑够kernel_size，来进行计算<br><img src="/images/Pasted%20image%2020260510105557.png" alt="526"></p><p>d 就是我们的膨胀率， 可以看到，</p><ul><li>d&#x3D;1的时候，膨胀卷积，就是我们普通的卷积计算，选择相邻的3隔输入序列（kernel_size&#x3D;3）</li><li>d&#x3D;2的时候，膨胀卷积，是选择的token步长2的3个token，（kernel_size &#x3D;3）</li><li>d&#x3D;4的时候，膨胀卷积，是选择的token步长4的3个token，（kernel_size &#x3D; 3）</li></ul><p><img src="/images/Pasted%20image%2020260510105824.png" alt="508"><br>换成计算角度，也就是在卷积核里面添加0，这样就可以实现token步长了</p><p><img src="/images/Pasted%20image%2020260510105901.png"></p><p><img src="/images/Pasted%20image%2020260510105913.png" alt="551"></p><p><mark style="background:#affad1">残差连接</mark><br><img src="/images/Pasted%20image%2020260510110309.png"></p><blockquote><p>和这里的CNN的通道理解是一样的，你的原始序列，就一个通道，但是经过一层卷积层之后（里面有多个卷积核），会产生多个通道的卷积后的序列。这里就是残差连接里面的部分了，通道数不一样了，所以，如果你这个时候，要残差链接，从旁路，把这多通道的序列，和原始单通道的序列汇合，肯定需要在1这个通路上，用一个1x1的卷积，调整输入序列的通道，变成和残差里面的通道数一致（也就是卷积核的个数）</p></blockquote><p><img src="/images/Pasted%20image%2020260510110352.png" alt="490"></p><h2 id="注意力机制"><a href="#注意力机制" class="headerlink" title="注意力机制"></a>注意力机制</h2><h3 id="注意力提示（Q，K，V）"><a href="#注意力提示（Q，K，V）" class="headerlink" title="注意力提示（Q，K，V）"></a>注意力提示（Q，K，V）</h3><p>首先，我们人类的注意力方式，分为主动和被动：</p><ul><li><strong>被动注意（非自主性注意）</strong>：<ul><li>类似被鲜艳的东西吸引，偏向感官输入（<strong>值</strong>）</li><li>每个值都有对应的键配对（<strong>键</strong>相当于这些值的都有的<strong>一个索引目录</strong>）</li><li>纯粹从全连接层的输入特征显示出来的焦点（左上角比较红，特征多这种）</li></ul></li><li><strong>主动注意（自主性注意）</strong>：<ul><li>虽然我们看到了有焦点的画面，但是我们有自己的意志，想要看右下角的不起眼的内容</li><li>通过<strong>注意力汇聚层</strong>， 把<strong>查询</strong>（自主性提示）与 <strong>键</strong>（索引目录）进行匹配，从而引导得出<strong>最匹配的值（感官输入）</strong></li></ul></li></ul><p>意思就是假设我们的眼睛是全连接，卷积，出来的某一层的特征就是值V，然后对应的索引就是键K，我们的查询Q和K，通过注意力汇聚层，就是我不看内容V，我只根据目录K，来加入我的意图，也就是注意力，这样Q，K 就形成了类似一个掩码mask层，对V进行过滤</p><p><img src="/images/Pasted%20image%2020260508113823.png" alt="460"></p><p><img src="/images/Pasted%20image%2020260508114210.png" alt="572"></p><p><img src="/images/Pasted%20image%2020260508114139.png" alt="583"></p><p><img src="/images/Pasted%20image%2020260508114231.png" alt="520"></p><h3 id="注意力汇聚（Watson-核回归）"><a href="#注意力汇聚（Watson-核回归）" class="headerlink" title="注意力汇聚（Watson 核回归）"></a>注意力汇聚（Watson 核回归）</h3><p>前面提到Q和K，再通过注意力汇聚，从而得到一张<strong>掩码（注意力汇聚）</strong>。</p><p>这一届，主要介绍<strong>Nadaraya-Watson</strong> <strong>核回归模型</strong></p><p>现在假设有50个样本的训练集训练的模型f(x)，我现在输入一个新的x， 我期待他能回归出来一个ypred</p><p><mark style="background:#affad1">平均汇聚</mark><br>如果是平均汇聚，那么我的f(x)无论输入什么x，回归出来的预测，都是训练集的标签的均值</p><p><mark style="background:#affad1">非参数注意力汇聚</mark><br>Watson他们提出了一个回归方法，就是如何把训练集的50个样本来训练后，根据输入的未知，对训练集标签，进行加权</p><p><img src="/images/Pasted%20image%2020260508132827.png" alt="367"></p><p>其中这个<strong>K（）</strong> 就是<strong>核（kernel）</strong>， 这个公式所描述的一个估计器（模型），被称为 <strong>Nadaraya-Watson 核回归</strong></p><p>受这个的启发，我们回到前面注意力机制框架的角度，重写这个核回归的估计器，从而得到，一个更加通用的<mark style="background:#fff88f">注意力汇聚</mark>的公式</p><p><img src="/images/Pasted%20image%2020260508133041.png" alt="211"></p><p>其中，x是查询，（xi,yi）是键值对。 这个f(x)输出就是注意力汇聚，也就是<strong>加上我们的掩码之后的输出</strong></p><p><strong>所以本质上还是加权平均，对所有的值yi，乘上权重α（Q，K）</strong>（注意力权重）</p><p>所以，这个权重，会被分配给每一个对应的值yi。 对任何查询x(Q), 模型在所有键值对注意力权重α，都是一个有效的概率分布，也就是说，每个值V的<strong>权重α（Q，K）</strong>：</p><ul><li>非负</li><li>和为1</li></ul><p>我们使用一个高斯核，</p><p><img src="/images/Pasted%20image%2020260508133447.png" alt="227"><br>这个核，利用上面的式子，可以组合出α权重，然后乘到每个值上去，最终得到：<br><img src="/images/Pasted%20image%2020260508133557.png" alt="367"></p><p>可以看到就是<code>∑ softmax(-0.5(Q-Ki)2) Vi</code></p><p>也就是说，如果一个键Ki(xi), 越是接近给定的查询x(Q), 那么分配给这个值yi（Vi）的注意力权重就会越大。也就获得了更多的注意力。</p><p>此时我们的Nadaraya-Watson核回归是一个非参数模型，所以上面的softmax的构成的注意力汇聚层，实际上，是<strong>非参数的注意力汇聚模型</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">创建数据集</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">n_train = <span class="number">50</span> <span class="comment"># 训练集 50个样本</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练集的输入，随机产生，然后按照从小到大的排序</span></span><br><span class="line">x_train, _ = torch.sort(torch.rand(n_train)*<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">f</span>(<span class="params">x</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span>*torch.sin(x) + x**<span class="number">0.8</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 训练集的标签，并添加高斯噪声</span></span><br><span class="line">y_train = f(x_train) + torch.normal(<span class="number">0.0</span>, <span class="number">0.5</span>, (n_train,))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试集</span></span><br><span class="line">x_test = torch.arange(<span class="number">0</span>,<span class="number">5</span>,<span class="number">0.1</span>)</span><br><span class="line">y_truth = f(x_test)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">plot_kernel_reg</span>(<span class="params">y_hat</span>):</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 预测结果</span></span><br><span class="line">    d2l.plt.plot(x_test, y_hat, <span class="string">&#x27;o&#x27;</span>, alpha=<span class="number">0.9</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 测试集</span></span><br><span class="line">    d2l.plt.plot(x_test, y_truth, <span class="string">&#x27;o&#x27;</span>, alpha=<span class="number">0.5</span>)</span><br><span class="line">    <span class="string">&quot;&quot;&quot;d2l.plot(x_test, [y_truth, y_hat], &#x27;x&#x27;, &#x27;y&#x27;, legend=[&#x27;Truth&#x27;, &#x27;Pred&#x27;],</span></span><br><span class="line"><span class="string">             xlim=[0, 5], ylim=[-1, 5])&quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 训练集</span></span><br><span class="line">    d2l.plt.plot(x_train, y_train, <span class="string">&#x27;o&#x27;</span>, alpha=<span class="number">0.5</span>)</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 class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">查询Q：x_test, 测试集的输入</span></span><br><span class="line"><span class="string">键K: x_train, 训练集的输入</span></span><br><span class="line"><span class="string">值V：y_train, 训练集的标签</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">X_repeat = x_test.repeat_interleave(n_train).reshape((-<span class="number">1</span>, n_train))</span><br><span class="line"><span class="comment"># X_repeat (n_test, n_train), 还是n_test个查询，只不过把每个查询Q，复制成len=n_train的张量</span></span><br><span class="line"><span class="built_in">print</span>(X_repeat)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">x_test = Q 是(n_test, )</span></span><br><span class="line"><span class="string">x_train = K 是(n_train, )</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">X_repeat:</span></span><br><span class="line"><span class="string">[Q1, Q1, ....., Q1] (n_train列)</span></span><br><span class="line"><span class="string">[Q2, Q2, ....., Q2]</span></span><br><span class="line"><span class="string">...</span></span><br><span class="line"><span class="string">[Qn_test, Qn_test, ....]</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 注意力权重α（Q，Ki）</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">x_train (n_train, ) = [K1, K2, K3, .... Kn_train]</span></span><br><span class="line"><span class="string">广播之后：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">x_train:</span></span><br><span class="line"><span class="string">[K1, K2, K3, .... Kn_train]</span></span><br><span class="line"><span class="string">[K1, K2, K3, .... Kn_train]</span></span><br><span class="line"><span class="string">...</span></span><br><span class="line"><span class="string">[K1, K2, K3, .... Kn_train]</span></span><br><span class="line"><span class="string">n_test行</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">所以这样形状都是(n_test, n_train), 就可以用矩阵来并行计算了，不用因为两个张量形状不同for循环</span></span><br><span class="line"><span class="string">X_repeat - x_train = 就是</span></span><br><span class="line"><span class="string">[Q1 - K1, Q1 - K2, ...., Q1 - Kn_train]</span></span><br><span class="line"><span class="string">[Q2 - K1, Q2 - K2, ...., Q2 - Kn_train]</span></span><br><span class="line"><span class="string">...</span></span><br><span class="line"><span class="string">所以就可以实现对所有的测试集的Qi, 计算得到</span></span><br><span class="line"><span class="string">[Q1 - Ki]</span></span><br><span class="line"><span class="string">[Q2 - Ki]</span></span><br><span class="line"><span class="string">...</span></span><br><span class="line"><span class="string">就跟batch_size一个道理，一个Qi就是一个样本</span></span><br><span class="line"><span class="string">我们要计算测试集的每个样本输入Q，他关于所有的训练集的输入Kj的权重</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">attention_weights = nn.functional.softmax(-(X_repeat - x_train)**<span class="number">2</span> / <span class="number">2</span>, dim=<span class="number">1</span>)</span><br><span class="line"><span class="comment"># attention_weights (test_batch_size, n_train)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#print(attention_weights.shape)</span></span><br><span class="line">y_hat = torch.matmul(attention_weights, y_train)</span><br><span class="line"><span class="comment">#plot_kernel_reg(y_hat)</span></span><br></pre></td></tr></table></figure><p>就跟batch_size一个道理，一个Qi就是一个样本<br>我们要计算测试集的每个样本输入Q，他关于所有的训练集的输入Kj的权重</p><p>所以，我们的α（Q，Ki）,他的形状是（n_test, n_train）</p><ul><li>n_test 也就是测试集的样本数，test_batch_size</li><li>n_train 也就是训练集的样本数，train_batch_size</li><li>我们的<strong>小目标</strong>是给定一个测试集的输入Q，计算他关于所有训练集输入K的关联性（用softmax（平方差）表示），</li><li>我们的<strong>大目标</strong>：得到所有的测试集的输入Q，计算他们分别关于训练集输入K的关联性，也就是我们的注意力权重。</li></ul><p><img src="/images/e6cd8c468932108f3309dac3165528b4.jpg"></p><p><img src="/images/Pasted%20image%2020260508143820.png" alt="601"></p><p><strong>所以，这里用测试集，来当作我们的主动注意力的查询Q，可以看到，经过（Q，K）的注意力汇聚层之后，有点像是，让原来的上一层的输出V，按照我的主动注意力Q，调整一下权重变化，这样，得到我们的注意力汇聚</strong>， 故名思意，前一层的输出（V），融合了我的Q，得到了新的V’， 也就是注意力汇聚，y_hat</p><p><img src="/images/Pasted%20image%2020260508144055.png"></p><p>所以，attention_weights， 就是一个注意力权重</p><p><mark style="background:#affad1">带参数的注意力汇聚</mark></p><blockquote><p><strong>我们这里讲的是注意力汇聚层，地位有点像是CNN里面的池化层</strong></p></blockquote><p><strong>非参数</strong>的Nadaraya-Watson核回归具有_<strong>一致性_</strong>（consistency）的优点： 如果有足够的数据，此模型会收敛到最优结果</p><p>我们还是可以轻松地将<strong>可学习的参数</strong>集成到<strong>注意力汇聚</strong>中</p><p><img src="/images/Pasted%20image%2020260508144421.png" alt="551"></p><p>对（Q，Ki）的距离计算，乘上了一个系数w</p><p><mark style="background:#affad1">torch的小批量矩阵乘法bmm</mark><br><img src="/images/Pasted%20image%2020260508144907.png" alt="561"></p><p><img src="/images/Pasted%20image%2020260508145351.png" alt="378"><br>.unsqueeze(1), 在第一维插入1， 所以是(2,1,10)<br>.unsqueeze(-1). 在最后一维插入1，所以是(2, 10, 1)<br>所以最后小批量乘法是(2,1,1)</p><p><mark style="background:#affad1">定义模型</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">定义了一个注意力汇聚层，拥有一个参数w</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">NWKernelRegression</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.w = nn.Parameter(torch.rand((<span class="number">1</span>,), requires_grad=<span class="literal">True</span>))</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 定义了注意力汇聚层的一次前向传播</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, queries, keys, values</span>):</span><br><span class="line">        <span class="comment"># queries和attention_weights的形状为(查询个数，“键－值”对个数)</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">        queries (n_test, n_train) (查询个数，“键－值”对个数)</span></span><br><span class="line"><span class="string">        attention_weights (n_test, n_train ) (查询个数，“键－值”对个数)</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        queries = queries.repeat_interleave(keys.shape[<span class="number">1</span>]).reshape((-<span class="number">1</span>, keys.shape[<span class="number">1</span>]))</span><br><span class="line">        <span class="variable language_">self</span>.attention_weights = nn.functional.softmax(</span><br><span class="line">            -((queries - keys) * <span class="variable language_">self</span>.w)**<span class="number">2</span> / <span class="number">2</span>, dim=<span class="number">1</span>)</span><br><span class="line">        <span class="comment"># values的形状为(查询个数，“键－值”对个数)</span></span><br><span class="line">        <span class="keyword">return</span> torch.bmm(<span class="variable language_">self</span>.attention_weights.unsqueeze(<span class="number">1</span>),</span><br><span class="line">                         values.unsqueeze(-<span class="number">1</span>)).reshape(-<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p><mark style="background:#affad1">训练</mark><br>用训练数据集的留一法，构造Q，K，V</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># X_tile的形状:(n_train，n_train)，每一行都包含着相同的训练输入</span></span><br><span class="line">X_tile = x_train.repeat((n_train, <span class="number">1</span>))</span><br><span class="line"><span class="comment"># Y_tile的形状:(n_train，n_train)，每一行都包含着相同的训练输出</span></span><br><span class="line">Y_tile = y_train.repeat((n_train, <span class="number">1</span>))</span><br><span class="line"><span class="comment"># keys的形状:(&#x27;n_train&#x27;，&#x27;n_train&#x27;-1)</span></span><br><span class="line">keys = X_tile[(<span class="number">1</span> - torch.eye(n_train)).<span class="built_in">type</span>(torch.<span class="built_in">bool</span>)].reshape((n_train, -<span class="number">1</span>))</span><br><span class="line"><span class="comment"># values的形状:(&#x27;n_train&#x27;，&#x27;n_train&#x27;-1)</span></span><br><span class="line">values = Y_tile[(<span class="number">1</span> - torch.eye(n_train)).<span class="built_in">type</span>(torch.<span class="built_in">bool</span>)].reshape((n_train, -<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">net = NWKernelRegression()</span><br><span class="line">loss = nn.MSELoss(reduction=<span class="string">&#x27;none&#x27;</span>)</span><br><span class="line">trainer = torch.optim.SGD(net.parameters(), lr=<span class="number">0.5</span>)</span><br><span class="line">animator = d2l.Animator(xlabel=<span class="string">&#x27;epoch&#x27;</span>, ylabel=<span class="string">&#x27;loss&#x27;</span>, xlim=[<span class="number">1</span>, <span class="number">5</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">5</span>):</span><br><span class="line">    trainer.zero_grad()</span><br><span class="line">    l = loss(net(x_train, keys, values), y_train)</span><br><span class="line">    l.<span class="built_in">sum</span>().backward()</span><br><span class="line">    trainer.step()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&#x27;epoch <span class="subst">&#123;epoch + <span class="number">1</span>&#125;</span>, loss <span class="subst">&#123;<span class="built_in">float</span>(l.<span class="built_in">sum</span>()):<span class="number">.6</span>f&#125;</span>&#x27;</span>)</span><br><span class="line">    animator.add(epoch + <span class="number">1</span>, <span class="built_in">float</span>(l.<span class="built_in">sum</span>()))</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260508150631.png" alt="476"></p><p><mark style="background:#affad1">测试注意力汇聚</mark></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">n_test = <span class="built_in">len</span>(x_test)</span><br><span class="line"></span><br><span class="line"><span class="comment"># keys的形状:(n_test，n_train)，每一行包含着相同的训练输入（例如，相同的键）</span></span><br><span class="line"></span><br><span class="line">keys = x_train.repeat((n_test, <span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"><span class="comment"># value的形状:(n_test，n_train)</span></span><br><span class="line"></span><br><span class="line">values = y_train.repeat((n_test, <span class="number">1</span>))</span><br><span class="line"></span><br><span class="line">y_hat = net(x_test, keys, values).unsqueeze(<span class="number">1</span>).detach()</span><br><span class="line"></span><br><span class="line">plot_kernel_reg(y_hat)</span><br></pre></td></tr></table></figure><p><img src="/images/Pasted%20image%2020260508151002.png" alt="418"></p><p><img src="/images/Pasted%20image%2020260508151328.png" alt="546"></p><h3 id="注意力评分函数（重要）"><a href="#注意力评分函数（重要）" class="headerlink" title="注意力评分函数（重要）"></a>注意力评分函数（重要）</h3><p>前面使用了高斯核来对查询和键之间的关系建模</p><p><img src="/images/Pasted%20image%2020260508152008.png" alt="547"></p><p> <a href="https://zh-v2.d2l.ai/chapter_attention-mechanisms/nadaraya-waston.html#equation-eq-nadaraya-watson-gaussian">(10.2.6)</a>中的 高斯核指数部分可以视为_注意力评分函数_（attention scoring function）， 简称_评分函数_（scoring function），然后把这个函数的输出结果输入到softmax函数中进行运算</p><p>通过上述步骤，将得到与键对应的<strong>值的概率分布</strong>（即<strong>注意力权重</strong>）。 最后，<strong>注意力汇聚的输出</strong>就是<strong>基于这些注意力权重的值的加权和</strong>。</p><p><img src="/images/Pasted%20image%2020260508152253.png"></p><p>从宏观来看，上述算法可以用来实现 <a href="https://zh-v2.d2l.ai/chapter_attention-mechanisms/attention-cues.html#fig-qkv">图10.1.3</a>中的注意力机制框架<br><img src="/images/Pasted%20image%2020260508152122.png"></p><p><img src="/images/Pasted%20image%2020260508152315.png" alt="657"></p><p>所以，q,k就是索引与索引之间的匹配关联程度，然后按照样本的标签，回归我们自己的预测。</p><p>softmax()里面的<strong>q,ki的计算方法</strong>，叫做<strong>评分函数</strong></p><p><mark style="background:#affad1">掩蔽softmax操作</mark><br>这个在<a href="#%E5%BA%8F%E5%88%97%E5%88%B0%E5%BA%8F%E5%88%97%E5%AD%A6%E4%B9%A0">序列到序列学习</a>这一节已经学过了</p><p>指定每个序列的有效token长度 &#x3D; valid_len 是一个list</p><p><img src="/images/Pasted%20image%2020260508152736.png" alt="619"></p><p><mark style="background:#affad1">加性注意力</mark> （当查询和键是<strong>不同长度的矢量时</strong>，可以使用可加性注意力评分函数）<br>一般来说，当<mark style="background:#fff88f">Q，K是不同长度的向量时</mark>，可以使用<strong>加性注意力</strong> 作为<strong>评分函数</strong></p><p><img src="/images/Pasted%20image%2020260508152916.png"></p><p>softmax(a(q,ki))</p><p>说白了，就是用全连接来实现维度的统一</p><p><mark style="background:#affad1">缩放点积注意力</mark>（当它们的长度相同时，使用<strong>缩放的“点－积”注意力评分函数</strong>的<strong>计算效率更高</strong>）</p><p><img src="/images/Pasted%20image%2020260508153147.png"></p><p><img src="/images/Pasted%20image%2020260508153334.png"><br><img src="/images/Pasted%20image%2020260508154750.png" alt="531"><br><img src="/images/5768d8a2677de272e295ed73cae332ad.jpg"><br><strong>缩放点积注意力权重</strong>的实现：包含评分函数+softmax</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment">#@save</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DotProductAttention</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;缩放点积注意力&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, dropout, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(DotProductAttention, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.dropout = nn.Dropout(dropout)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># queries的形状：(batch_size，查询的个数，d)</span></span><br><span class="line">    <span class="comment"># keys的形状：(batch_size，“键－值”对的个数，d)</span></span><br><span class="line">    <span class="comment"># values的形状：(batch_size，“键－值”对的个数，值的维度)</span></span><br><span class="line">    <span class="comment"># valid_lens的形状:(batch_size，)或者(batch_size，查询的个数)</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, queries, keys, values, valid_lens=<span class="literal">None</span></span>):</span><br><span class="line">        d = queries.shape[-<span class="number">1</span>]</span><br><span class="line">        <span class="comment"># 设置transpose_b=True为了交换keys的最后两个维度</span></span><br><span class="line">        scores = torch.bmm(queries, keys.transpose(<span class="number">1</span>,<span class="number">2</span>)) / math.sqrt(d)</span><br><span class="line">        <span class="variable language_">self</span>.attention_weights = masked_softmax(scores, valid_lens)</span><br><span class="line">        <span class="keyword">return</span> torch.bmm(<span class="variable language_">self</span>.dropout(<span class="variable language_">self</span>.attention_weights), values)</span><br></pre></td></tr></table></figure><p><mark style="background:#fff88f">热力图查看</mark><br><img src="/images/Pasted%20image%2020260508154134.png" alt="489"></p><hr><h3 id="Bahdanau-注意力"><a href="#Bahdanau-注意力" class="headerlink" title="Bahdanau 注意力"></a>Bahdanau 注意力</h3><p>前面，我们学会了一个层，叫做注意力汇聚层，然后我们里面使用的是缩放点积注意力评分函数</p><p>再前面的序列-序列的模型，通过设计一个<strong>基于两个循环神经网络的编码器-解码器架构</strong>， 用于<strong>序列到序列学习</strong></p><p>然而，即使<mark style="background:#fff88f">并非所有输入（源）词元都对解码某个词元都有用</mark>， 在每个解码步骤中仍使用编码_相同_的上下文变量。 <mark style="background:#fff88f">有什么方法能改变上下文变量呢</mark>？</p><blockquote><p>原来的上下文变量C，里面包含了所有的源序列的token的信息，但是不是所有的源序列的token都有用，所有在这里做一点改变</p></blockquote><p>Bahdanau等人提出了一个<strong>没有严格单向对齐限制</strong>的 <strong>可微注意力模型</strong></p><p><mark style="background:#fff88f">在预测词元时，如果不是所有输入词元都相关，模型将仅对齐（或参与）输入序列中与当前预测相关的部分，这是通过将上下文变量视为注意力集中的输出来实现的</mark></p><p><strong>下面描述的Bahdanau注意力模型</strong></p><p>这个新的基于注意力的模型与 <a href="https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html#sec-seq2seq">9.7节</a>中的模型相同， 只不过 <a href="https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html#equation-eq-seq2seq-s-t">(9.7.3)</a>中的上下文变量C，在任何解码器的时间步t’， 都会被Ct’替换（<strong>就是解码器在自回归的过程中，也会调整上下文变量C</strong>）</p><p>假设解码器的输入序列是T个token， 解码时间步t’ 的上下文更新后 &#x3D; 注意力集中的输出：<br><img src="/images/Pasted%20image%2020260508160645.png"></p><p>st’-1, 是<strong>查询</strong>&#x3D;<strong>解码器上一时刻的隐状态</strong>，<br><strong>编码器隐状态ht</strong> 视为键和值）</p><blockquote><p>编码器在解码器运行前，就已经跑完了，所以他的<strong>所有层的最终状态</strong>，作为<strong>解码器的初始状态</strong>。也就是我们的K，V</p></blockquote><p><img src="/images/Pasted%20image%2020260508161120.png" alt="499"></p><p>所以这样的注意力模型，如上所示。</p><p>在大模型里面，RNN的编码器解码器已经不用了，现在主流是transformer，所以这一节不算特别重要</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"><span class="keyword">from</span> d2l <span class="keyword">import</span> torch <span class="keyword">as</span> d2l</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AttentionDecoder</span>(d2l.Decoder):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;带有注意力机制解码器的基本接口&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(AttentionDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">attention_weights</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">raise</span> NotImplementedError</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Seq2SeqAttentionDecoder</span>(<span class="title class_ inherited__">AttentionDecoder</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embed_size, num_hiddens, num_layers,</span></span><br><span class="line"><span class="params">                 dropout=<span class="number">0</span>, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(Seq2SeqAttentionDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 注意力集中层，利用编码器的状态，以及解码器的当前状态，产生上下文变量</span></span><br><span class="line">        <span class="comment"># 这里使用的是加性注意力，不是缩放点积注意力</span></span><br><span class="line">        <span class="variable language_">self</span>.attention = d2l.AdditiveAttention(</span><br><span class="line">            num_hiddens, dropout, **kwargs)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 嵌入层，用来把token转换成(embed_size, )的格式</span></span><br><span class="line">        <span class="variable language_">self</span>.embedding = nn.Embedding(vocab_size, embed_size)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 解码器的循环神经网络</span></span><br><span class="line">        <span class="comment"># 输入：(batch_size, y_seq_len, embed_size + hidden_size)</span></span><br><span class="line">        <span class="variable language_">self</span>.rnn = nn.GRU(</span><br><span class="line">            embed_size + num_hiddens, num_hiddens, num_layers,</span><br><span class="line">            dropout=dropout)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 产生token种类的打分的全连接层</span></span><br><span class="line">        <span class="variable language_">self</span>.dense = nn.Linear(num_hiddens, vocab_size)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_state</span>(<span class="params">self, enc_outputs, enc_valid_lens, *args</span>):</span><br><span class="line">        <span class="comment"># outputs的形状为(batch_size，num_steps，num_hiddens).</span></span><br><span class="line">        <span class="comment"># hidden_state的形状为(num_layers，batch_size，num_hiddens)</span></span><br><span class="line">        outputs, hidden_state = enc_outputs</span><br><span class="line">        <span class="keyword">return</span> (outputs.permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>), hidden_state, enc_valid_lens)</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    编码器的输入</span></span><br><span class="line"><span class="string">    X（batch_size, seq_len）</span></span><br><span class="line"><span class="string">    state (layer_num, batch_size, hidden_size)</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, X, state</span>):</span><br><span class="line">        enc_outputs, hidden_state, enc_valid_lens = state</span><br><span class="line">        <span class="comment"># enc_outputs的形状为(batch_size,num_steps,num_hiddens). 为编码器最后一层隐状态的所有历史状态</span></span><br><span class="line">        <span class="comment"># hidden_state的形状为(num_layers,batch_size, num_hiddens)，为编码器所有层最后一个时间步的状态</span></span><br><span class="line">        </span><br><span class="line">        X = <span class="variable language_">self</span>.embedding(X).permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        <span class="comment"># 输出X的形状为(num_steps,batch_size,embed_size), 可以开始内部循环自回归了</span></span><br><span class="line"></span><br><span class="line">        outputs, <span class="variable language_">self</span>._attention_weights = [], []</span><br><span class="line">        <span class="keyword">for</span> x <span class="keyword">in</span> X:</span><br><span class="line">            <span class="comment"># 对于每一个时间步，x (batch_size, embed_size)</span></span><br><span class="line">            <span class="comment"># query的形状为(batch_size,1,num_hiddens)</span></span><br><span class="line">            <span class="comment"># hidden_state的形状为(num_layers,batch_size, num_hiddens)</span></span><br><span class="line">            query = torch.unsqueeze(hidden_state[-<span class="number">1</span>], dim=<span class="number">1</span>)</span><br><span class="line">            <span class="comment"># query(batch_size, 1, hidden_size),为编码器最后一层隐状态最终时间步的状态</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            <span class="comment"># context的形状为(batch_size,1,num_hiddens)</span></span><br><span class="line">            <span class="comment"># query就是Q，enc_outputs就是K，enc_outputs就是V</span></span><br><span class="line">            <span class="comment"># K,V = enc_outputs的形状为(batch_size,num_steps,num_hiddens).</span></span><br><span class="line">            context = <span class="variable language_">self</span>.attention(</span><br><span class="line">                query, enc_outputs, enc_outputs, enc_valid_lens)</span><br><span class="line">            <span class="comment"># context 就是 ct&#x27;, 为解码器该时间步的注意力集中的输出</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 在特征维度上连结</span></span><br><span class="line">            x = torch.cat((context, torch.unsqueeze(x, dim=<span class="number">1</span>)), dim=-<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 将x变形为(1,batch_size,embed_size+num_hiddens)</span></span><br><span class="line">            out, hidden_state = <span class="variable language_">self</span>.rnn(x.permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>), hidden_state)</span><br><span class="line"></span><br><span class="line">            outputs.append(out)</span><br><span class="line">            <span class="variable language_">self</span>._attention_weights.append(<span class="variable language_">self</span>.attention.attention_weights)</span><br><span class="line">        <span class="comment"># 全连接层变换后，outputs的形状为</span></span><br><span class="line">        <span class="comment"># (num_steps,batch_size,vocab_size)</span></span><br><span class="line">        outputs = <span class="variable language_">self</span>.dense(torch.cat(outputs, dim=<span class="number">0</span>))</span><br><span class="line">        <span class="keyword">return</span> outputs.permute(<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>), [enc_outputs, hidden_state,</span><br><span class="line">                                          enc_valid_lens]</span><br><span class="line"></span><br><span class="line"><span class="meta">    @property</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">attention_weights</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>._attention_weights</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,</span></span><br><span class="line"><span class="string">                             num_layers=2)</span></span><br><span class="line"><span class="string">encoder.eval()</span></span><br><span class="line"><span class="string">decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16,</span></span><br><span class="line"><span class="string">                                  num_layers=2)</span></span><br><span class="line"><span class="string">decoder.eval()</span></span><br><span class="line"><span class="string">X = torch.zeros((4, 7), dtype=torch.long)  # (batch_size,num_steps)</span></span><br><span class="line"><span class="string">state = decoder.init_state(encoder(X), None)</span></span><br><span class="line"><span class="string">output, state = decoder(X, state)</span></span><br><span class="line"><span class="string">print(output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape)</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></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 class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">训练</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">EncoderDecoder</span>(nn.Module):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;编码器-解码器架构的基类&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, encoder, decoder, **kwargs</span>):</span><br><span class="line">        <span class="built_in">super</span>(EncoderDecoder, <span class="variable language_">self</span>).__init__(**kwargs)</span><br><span class="line">        <span class="variable language_">self</span>.encoder = encoder</span><br><span class="line">        <span class="variable language_">self</span>.decoder = decoder</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, enc_X, dec_X, *args</span>):</span><br><span class="line">        enc_outputs = <span class="variable language_">self</span>.encoder(enc_X)</span><br><span class="line">        dec_state = <span class="variable language_">self</span>.decoder.init_state(enc_outputs, *args)</span><br><span class="line">        dec_outputs, dec_state = <span class="variable language_">self</span>.decoder(dec_X, dec_state)</span><br><span class="line">        <span class="comment"># 这里只返回 2 个值：预测输出和解码器状态</span></span><br><span class="line">        <span class="keyword">return</span> dec_outputs, dec_state</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line"></span><br><span class="line">embed_size, num_hiddens, num_layers, dropout = <span class="number">32</span>, <span class="number">32</span>, <span class="number">2</span>, <span class="number">0.1</span></span><br><span class="line">batch_size, num_steps = <span class="number">64</span>, <span class="number">10</span></span><br><span class="line">lr, num_epochs, device = <span class="number">0.005</span>, <span class="number">250</span>, d2l.try_gpu()</span><br><span class="line"></span><br><span class="line">train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)</span><br><span class="line">encoder = d2l.Seq2SeqEncoder(</span><br><span class="line">    <span class="built_in">len</span>(src_vocab), embed_size, num_hiddens, num_layers, dropout)</span><br><span class="line">decoder = Seq2SeqAttentionDecoder(</span><br><span class="line">    <span class="built_in">len</span>(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)</span><br><span class="line">net = EncoderDecoder(encoder, decoder)</span><br><span class="line">d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;测试&quot;&quot;&quot;</span></span><br><span class="line">engs = [<span class="string">&#x27;go .&#x27;</span>, <span class="string">&quot;i lost .&quot;</span>, <span class="string">&#x27;he\&#x27;s calm .&#x27;</span>, <span class="string">&#x27;i\&#x27;m home .&#x27;</span>]</span><br><span class="line">fras = [<span class="string">&#x27;va !&#x27;</span>, <span class="string">&#x27;j\&#x27;ai perdu .&#x27;</span>, <span class="string">&#x27;il est calme .&#x27;</span>, <span class="string">&#x27;je suis chez moi .&#x27;</span>]</span><br><span class="line"><span class="keyword">for</span> eng, fra <span class="keyword">in</span> <span class="built_in">zip</span>(engs, fras):</span><br><span class="line">    translation, dec_attention_weight_seq = d2l.predict_seq2seq(</span><br><span class="line">        net, eng, src_vocab, tgt_vocab, num_steps, device, <span class="literal">True</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&#x27;<span class="subst">&#123;eng&#125;</span> =&gt; <span class="subst">&#123;translation&#125;</span>, &#x27;</span>,</span><br><span class="line">          <span class="string">f&#x27;bleu <span class="subst">&#123;d2l.bleu(translation, fra, k=<span class="number">2</span>):<span class="number">.3</span>f&#125;</span>&#x27;</span>)</span><br><span class="line">    </span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="自注意力-位置编码"><a href="#自注意力-位置编码" class="headerlink" title="自注意力+位置编码"></a>自注意力+位置编码</h3><p><img src="/images/Pasted%20image%2020260509162707.png"><br><img src="/images/Pasted%20image%2020260509162715.png"></p><p><img src="/images/Pasted%20image%2020260509162733.png"><br>就是对X增加了一层摩尔纹意义，这个sin，cos的设计，主要目的是为了模仿二进制编码的变化频率，（地位变化频率高，高位变化频率低），简单说就是：<br>该序列的第i个token，他有d维，j∈d，然后按照奇数偶数的不同来得到一个固定的编码值，他的好处是，在j，也就是d维里面比较低的位置，变化的频率快，高维的位置，变化的频率慢（模拟二进制编码的变化）</p><p><img src="/images/Pasted%20image%2020260509163035.png" alt="453"></p><p><img src="/images/Pasted%20image%2020260509163147.png"><br>可以看到，一个token的d位，低位变化密集，高位变化慢，<strong>本质上就是叠了一层二进制地址</strong>（address + value， 可以理解为，在这块内存的数据上，加了内存的地址），上图就是模拟二进制地址本身热力图的变化</p><h3 id="多头注意力（重要）"><a href="#多头注意力（重要）" class="headerlink" title="多头注意力（重要）"></a>多头注意力（重要）</h3><p><img src="/images/Pasted%20image%2020260508202017.png" alt="614"></p><p>使用的是Q，K，V的不同子空间的表示。</p><p>也就是，把QKV，进入MLP，投影到不同的特征空间，然后再进入注意力层回归出我们需要的Q的V‘</p><p>原来我们的注意力层：h &#x3D; f(q,k,v)&#x3D;f(q, (k1,v1), (k2,v2), …,(kn,vn))<br>现在多头注意力的一个头是hi &#x3D; f(Wi(q)q, Wi(k)k, Wi(v)v)<br>也就是每个头，q,k,v都要经过一个独立的全连接层，投影到相同的h维度的特征空间上，然后在完成后续的注意力计算。</p><p><img src="/images/Pasted%20image%2020260509162016.png"></p><p>所以，一共有hx3个参数需要学习<br>f的注意力计算，我们一般使用加性注意力（带参：wv*  tanh(wqxq + wk x k)）&#x2F;缩放点积注意力(无参)<br><img src="/images/Pasted%20image%2020260509162022.png"></p><p><img src="/images/Pasted%20image%2020260509162637.png" alt="659"></p><p><img src="/images/Pasted%20image%2020260509211544.png"></p><p><img src="/images/Pasted%20image%2020260509211549.png" alt="494"><br><img src="/images/Pasted%20image%2020260509211558.png" alt="592"></p><p>我理解了，因为每个头是并行的存在，所以，我为了并行计算，所以，必须用一个大矩阵Wq &#x3D; [Wq1, Wq2, Wq3,….,Wqh]来同时作用在q上，得到[q1, q2, q3, …, qh] 就可以实现多头的并行。</p><p>所以我的全连接的输出hidden_size &#x3D; num_heads * query_size 是吗？</p><p>那这样的话，如果q,k,v不是一个d， 那岂不是，Q，K，V的全连接输出的hidden_size, 也是不一样的？</p><p>然后呢，我为什么需要使用transpose_qkv，来把这一整个的[q1, q2, q3, …, qh]，拆分成h个头的专属q,k,v? 是因为不能让头1的q，也算到头2的KV是吧</p><p>我这里还有一个疑问，既然是并行计算每个头的pq, pk, pv, 那我直接设置一个大的Wq &#x3D; [Wq1, Wq2, Wq3,….,Wqh], 因为我初始化不一样，所以里面每个头的全连接的参数都不一样，</p><p><img src="/images/Pasted%20image%2020260509212241.png"></p><p><img src="/images/bf47cdcc441370097dcd7273eb7c1003.jpg"><br><img src="/images/5ee6617adde3a3448d208e1a2988c0f2.jpg"><br><img src="/images/5d17e7458effb36eea15d88152492ee2.jpg"><br><img src="/images/15de1a2d460f73003713319c82f74d9f.jpg"><br><strong>全连接变维度，又是每个头只注意高维空间的部分维，最后又全连接整合映射到一个低维上</strong></p><p>是：<strong>先拆分升维做多视角建模，再拼接降维做特征融合</strong></p><h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p><mark style="background:#affad1">朴素注意力机制</mark><br>所以注意力机制，依赖（K，V）数据库，以及我们的输入查询Q，</p><p>他的作用是，通过计算（K，V）中，不同的ki, 对 Q的贡献程度，来实现输入Q的回归预测<br>注意力权重，就是显示Q和Ki们的贡献程度</p><p>所以注意力机制，是没有模型参数的</p><p><mark style="background:#affad1">全连接层</mark>，则是把数据的关系，全部训练到模型参数里面</p><p><mark style="background:#affad1">transformer里面的注意力</mark>（多头注意力）</p><p>所以实际的transformer注意力里面，是<strong>先用全连接，把Q，K，V映射到合适做注意力回归的空间</strong>（MLP来改变数据维数，也就是空间变形，投影）然后在这个特征空间里面，再来做朴素注意力回归</p><p><mark style="background:#affad1">Bahdanau注意力</mark><br>原来seq2seq，使用到编码器-解码器的架构，里面用到两层的GRU，此时他的模型参数，主要是里面的RNN的模型参数，他的主要两个设计点是： </p><ul><li>编码器最后一层隐藏层的最后一个时间步的状态hT 作为上下文变量context </li><li>编码器所有隐藏层的最后一个时间步的状态[hT，hT], 作为解码器的隐藏层的初始状态[s0, s0]</li></ul><p>然后再bahdanau注意力里面，通过引入注意力层，来改进这个seq2seq的模型 </p><p>他的改进是： </p><ul><li>上下文变量，不再是一成不变的，而是，我在输入解码器的x1’之前，编码器已经把源序列都输入完成了</li></ul><p>因此，我们已经得到了编码器最后一层的所有时间步的状态[h1,h2,…,hT], 我们把这个状态作为K，V，把解码器在接受输入xt’时，他的上一时刻的状态st’-1, 所谓查询，把q,K,V 作为我们的注意力层的输入（这里有一个细节，就是纳入KV的计算，是源序列的所有T时间步的KV）从而得到真实的，根据我们当前解码器的时间步t’的，上下文变量context，然后和输入xt‘合并，输入解码器</p><p><img src="/images/Pasted%20image%2020260509153537.png" alt="590"></p><p><img src="/images/Pasted%20image%2020260509153503.png" alt="499"></p><p><mark style="background:#affad1">编码器遮罩（填充掩码：enc_valid_len）</mark></p><p><img src="/images/Pasted%20image%2020260509153851.png" alt="530"></p><p><mark style="background:#affad1">解码器自身的遮罩</mark><br>这个是<strong>自注意力</strong>，<strong>不能让当前时间步，看到未来时刻的token</strong></p><p><mark style="background:#affad1">自注意力</mark></p><blockquote><p>自注意力层，我知道他计算出的自注意力权重，可以反应一个序列中，一个token和其他token的相关程度。</p><p> 但是我想知道，这个自注意力权重，乘上V（整个序列）之后，有什么意义呢？这个新的序列，有什么意义呢？</p></blockquote><p><img src="/images/Pasted%20image%2020260509155740.png" alt="456"></p><p><img src="/images/Pasted%20image%2020260509155755.png" alt="455"></p><p>所以作用就是，<mark style="background:#fff88f">让这个token，里面能够包含整个序列的上下文信息</mark><br>（对比RNN，只能从左到右挨个计算攒上下文，远距离的依赖弱<br>自注意力，直接并行计算，可以把序列内全部的token信息，全部融合到每个token里面<br>）</p><p>但是，这个自注意力层，<strong>有一个问题</strong>：<mark style="background:#fff88f">就是没有未知关系。只有相对的token2token的关系，没有第一位，第二位的时序关系。</mark></p><p>所以，才需要，<mark style="background:#affad1">位置编码</mark></p><p><strong>位置编码可以通过学习得到也可以直接固定得到</strong></p><p><mark style="background:#affad1">掩码多头自注意力</mark>：</p><p><strong>解码器的第一层</strong>，是掩码自注意力，如果没有掩码，他就会直接计算目标序列的所有token关于其他全部token的关联，加上掩码之后，只计算该目标序列和之前目标序列的相关程度<br><img src="/images/Pasted%20image%2020260511131945.png" alt="307"></p><p><strong>我们先讨论训练情况，我一次性输入目标序列，然后多头掩码自注意力，计算第i个token和和前面i-1个token的相关性，作为这个token的注意力权重，所以这个多头掩码自注意力，输出的还是一个序列</strong></p><p><img src="/images/Pasted%20image%2020260511133956.png" alt="570"></p><p><strong>所以transformer自回归，因为有多头自注意力开头的原因，所以，每次自回归，都是把自己预测出来的token，和之前的历史token序列拼起来，然后再输送回解码器开头，增加计算这个token关于历史token的自注意力</strong></p><h3 id="QA"><a href="#QA" class="headerlink" title="QA"></a>QA</h3><blockquote><p>我想知道，在训练阶段，是一口气输入目标序列， 经过掩码多头自注意力后，生成的序列，每个token: </p><ol><li>知道自己和之前的token的关系（掩码自注意） </li><li>每个token知道自己更高维的特征（多头） 是吧</li></ol></blockquote><blockquote><ol><li>我很好奇，我要如何理解这个掩码多头自注意力的输出序列的数学含义呢？（就是我好奇他训练阶段，一口气输入，然后最终解码器产生的序列也是一口气输出，就是整个输入序列的后一个token的预测是吧。这个后一个，是由掩码来决定的）</li></ol></blockquote><blockquote><p>然后这个掩码多头自注意力的输出序列，（里面携带了哪些信息？）输入多头注意力，KV是编码器对源序列的处理结果（该KV序列，蕴含了原始序列的哪些信息？我要如何形象的理解？） </p></blockquote><blockquote><p>然后呢，这个多头注意力层，以编码器的输入的序列的掩码多头自注意力的序列维输入，作为Q，我想知道，用目标序列作为查询，原始序列作为键值，输出的物理含义，我又如何形象的理解，最后的FFN又是想干嘛？他的目的是为了什么？是为了学习在编码后的语义空间里面，源序列和目标序列的一个预测的分布关系吗</p></blockquote><p><img src="/images/Pasted%20image%2020260511151516.png"><br><mark style="background:#fff88f">所以，掩码多头自注意力的输出序列，<strong>每个 token 的向量，已经融合了 “它前面所有 token 的上下文信息 + 多个维度的语义关系”</strong>，但依然保持序列的长度不变</mark></p><p><img src="/images/Pasted%20image%2020260511151617.png" alt="588"></p><p><img src="/images/Pasted%20image%2020260511151704.png"></p><p><img src="/images/Pasted%20image%2020260511151839.png"></p><p><img src="/images/Pasted%20image%2020260511152501.png" alt="676"><br><img src="/images/Pasted%20image%2020260511152951.png" alt="384"><br>注意，这里是中间有一个非线性</p><p><img src="/images/Pasted%20image%2020260511153031.png" alt="604"></p><p><img src="/images/Pasted%20image%2020260511153200.png" alt="570"><br><img src="/images/Pasted%20image%2020260511153247.png" alt="518"></p><p><mark style="background:#affad1">总的来看，解码器的输入输出（训练阶段）</mark><br><img src="/images/Pasted%20image%2020260511153631.png"></p><p><img src="/images/Pasted%20image%2020260511153702.png" alt="257"></p><p><mark style="background:#affad1">解码器的输出和模型的预测输出</mark></p><p><img src="/images/Pasted%20image%2020260511153944.png"></p><h3 id="transformer架构认知修正"><a href="#transformer架构认知修正" class="headerlink" title="transformer架构认知修正"></a>transformer架构认知修正</h3><p><img src="/images/Pasted%20image%2020260511154249.png"></p><p><img src="/images/Pasted%20image%2020260511154300.png" alt="616"></p><ul><li>编码器和解码器，本质上都是<strong>特征处理器</strong>，输出的是语义向量，不是词。</li><li>真正的 “预测”，是靠解码器后面的线性层，把向量投影到词表空间，再通过 Softmax 变成概率。</li></ul><p><img src="/images/Pasted%20image%2020260511154949.png" alt="627"></p><p><img src="/images/Pasted%20image%2020260511155033.png"><br><img src="/images/Pasted%20image%2020260511155123.png" alt="376"><br><img src="/images/Pasted%20image%2020260511155134.png" alt="457"><br><img src="/images/Pasted%20image%2020260511155150.png" alt="319"></p><p><img src="/images/Pasted%20image%2020260511155212.png"></p><h4 id="解码器的KVcache"><a href="#解码器的KVcache" class="headerlink" title="解码器的KVcache"></a>解码器的KVcache</h4><p><img src="/images/Pasted%20image%2020260511164945.png" alt="481"></p><p><img src="/images/Pasted%20image%2020260511165005.png" alt="457"></p><p><img src="/images/b7c4a7123d24f0e164c81cf416573970.jpg"><br><img src="/images/cf4bf1a10767c28fd2a4f0837d233871.jpg"></p><h1 id="torch-损失函数"><a href="#torch-损失函数" class="headerlink" title="torch 损失函数"></a>torch 损失函数</h1><h1 id="torch-训练"><a href="#torch-训练" class="headerlink" title="torch 训练"></a>torch 训练</h1><h1 id="torch-评估"><a href="#torch-评估" class="headerlink" title="torch 评估"></a>torch 评估</h1>]]></content>
    
    
    <summary type="html">从 Python 与 PyTorch 基础出发，记录自动微分、数据集、模型训练和常见网络结构的学习笔记。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="基础与训练" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%9F%BA%E7%A1%80%E4%B8%8E%E8%AE%AD%E7%BB%83/"/>
    
    
    <category term="PyTorch" scheme="https://liangji-seu.github.io/tags/PyTorch/"/>
    
    <category term="Python" scheme="https://liangji-seu.github.io/tags/Python/"/>
    
    <category term="深度学习" scheme="https://liangji-seu.github.io/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>ACM模式输入输出</title>
    <link href="https://liangji-seu.github.io/2026/04/14/leecode-ACM%E6%A8%A1%E5%BC%8F%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA/"/>
    <id>https://liangji-seu.github.io/2026/04/14/leecode-ACM%E6%A8%A1%E5%BC%8F%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA/</id>
    <published>2026-04-14T01:08:33.000Z</published>
    <updated>2026-07-18T08:12:34.412Z</updated>
    
    <content type="html"><![CDATA[<h1 id="ACM输入输出模式"><a href="#ACM输入输出模式" class="headerlink" title="ACM输入输出模式"></a>ACM输入输出模式</h1><p><img src="/images/ACM%E6%A8%A1%E5%BC%8F%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA-01-0428123440.png" alt="alt text"></p><h2 id="输入"><a href="#输入" class="headerlink" title="输入"></a>输入</h2><h3 id="指定输入个数"><a href="#指定输入个数" class="headerlink" title="指定输入个数"></a>指定输入个数</h3><p><img src="/images/ACM%E6%A8%A1%E5%BC%8F%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA-02-0428123440.png" alt="alt text"></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> n;</span><br><span class="line"><span class="built_in">cin</span> &gt;&gt; n; <span class="comment">// 读入3，说明数组的大小是3</span></span><br><span class="line"><span class="built_in">vector</span>&lt;<span class="type">int</span>&gt; <span class="title function_">nums</span><span class="params">(n)</span>; <span class="comment">// 创建大小为3的vector&lt;int&gt;</span></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; i++) &#123;</span><br><span class="line"><span class="built_in">cin</span> &gt;&gt; nums[i];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 验证是否读入成功</span></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; nums.size(); i++) &#123;</span><br><span class="line"><span class="built_in">cout</span> &lt;&lt; nums[i] &lt;&lt; <span class="string">&quot; &quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">cout</span> &lt;&lt; <span class="built_in">endl</span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="不指定输入个数，以换行符-“-n”-结束"><a href="#不指定输入个数，以换行符-“-n”-结束" class="headerlink" title="不指定输入个数，以换行符 “\n” 结束"></a>不指定输入个数，以换行符 “\n” 结束</h3><h2 id="输出"><a href="#输出" class="headerlink" title="输出"></a>输出</h2>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;ACM输入输出模式&quot;&gt;&lt;a href=&quot;#ACM输入输出模式&quot; class=&quot;headerlink&quot; title=&quot;ACM输入输出模式&quot;&gt;&lt;/a&gt;ACM输入输出模式&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;/images/ACM%E6%A8%A1%E5%BC%8F%E8</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="CPP" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/CPP/"/>
    
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="cpp" scheme="https://liangji-seu.github.io/tags/cpp/"/>
    
  </entry>
  
  <entry>
    <title>大模型学习项目1: 7B模型QLoRA指令微调(SFT)</title>
    <link href="https://liangji-seu.github.io/2026/04/12/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE1-7B%E6%A8%A1%E5%9E%8BQLoRA%E6%8C%87%E4%BB%A4%E5%BE%AE%E8%B0%83-SFT/"/>
    <id>https://liangji-seu.github.io/2026/04/12/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE1-7B%E6%A8%A1%E5%9E%8BQLoRA%E6%8C%87%E4%BB%A4%E5%BE%AE%E8%B0%83-SFT/</id>
    <published>2026-04-12T09:30:57.000Z</published>
    <updated>2026-10-03T10:34:31.630Z</updated>
    
    <content type="html"><![CDATA[<h1 id="项目1：7B模型QLoRA-指令微调-SFT"><a href="#项目1：7B模型QLoRA-指令微调-SFT" class="headerlink" title="项目1：7B模型QLoRA 指令微调(SFT)"></a>项目1：7B模型QLoRA 指令微调(SFT)</h1><p><img src="/images/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE1-7B%E6%A8%A1%E5%9E%8BQLoRA%E6%8C%87%E4%BB%A4%E5%BE%AE%E8%B0%83-SFT-01-0414090753.png" alt="alt text"></p><h2 id="概念扫盲"><a href="#概念扫盲" class="headerlink" title="概念扫盲"></a>概念扫盲</h2><p>Alpaca 格式 是最经典的指令微调格式。每一条数据通常包含：instruction, input, output</p><h2 id=""><a href="#" class="headerlink" title=""></a></h2><figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 2. 安装核心“四大件”</span></span><br><span class="line"><span class="comment"># transformers: 模型加载</span></span><br><span class="line"><span class="comment"># peft: LoRA 核心库</span></span><br><span class="line"><span class="comment"># bitsandbytes: 4-bit 量化工具（QLoRA 的灵魂）</span></span><br><span class="line"><span class="comment"># accelerate: 分布式训练加速</span></span><br><span class="line">pip install torch torchvision torchaudio</span><br><span class="line">pip install transformers peft bitsandbytes accelerate datasets trl</span><br></pre></td></tr></table></figure><p><strong>量化</strong>就是压缩模型体积，通过降低精度，来用更少的内存空间来存储模型</p><p><img src="/images/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE1-7B%E6%A8%A1%E5%9E%8BQLoRA%E6%8C%87%E4%BB%A4%E5%BE%AE%E8%B0%83-SFT-02-0414090753.png" alt="alt text"></p><ol><li><p><strong>为什么不能直接微调（全量微调）</strong>？<br>假设你想让 Qwen 学会说“扬州话”。在传统的深度学习里，你会更新 $\theta$ 里的所有参数。但对于 7B 模型：<br> 显存灾难：训练时不仅要存参数，还要存梯度（Gradients）和优化器状态（Optimizer States）。</p><p> 全量微调一个 7B 模型通常需要 80GB~100GB 的显存。灾难性遗忘：如果你用力过猛，模型学会了扬州话，但可能把怎么写 Python 代码给忘了。</p></li></ol><p>LoRA 的核心：外挂“低秩矩阵”，思路是不动原始参数 θ<br><img src="/images/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE1-7B%E6%A8%A1%E5%9E%8BQLoRA%E6%8C%87%E4%BB%A4%E5%BE%AE%E8%B0%83-SFT-03-0414090753.png" alt="alt text"></p>]]></content>
    
    
    <summary type="html">以 7B 模型为例，记录 QLoRA 指令微调（SFT）的数据格式、量化、LoRA 与训练依赖。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="大模型算法" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/"/>
    
    <category term="基础与训练" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AE%97%E6%B3%95/%E5%9F%BA%E7%A1%80%E4%B8%8E%E8%AE%AD%E7%BB%83/"/>
    
    
    <category term="大模型" scheme="https://liangji-seu.github.io/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="QLoRA" scheme="https://liangji-seu.github.io/tags/QLoRA/"/>
    
    <category term="LoRA" scheme="https://liangji-seu.github.io/tags/LoRA/"/>
    
    <category term="SFT" scheme="https://liangji-seu.github.io/tags/SFT/"/>
    
  </entry>
  
  <entry>
    <title>ROS2 完整学习指南</title>
    <link href="https://liangji-seu.github.io/2026/03/25/ROS2-%E5%AE%8C%E6%95%B4%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%8D%97/"/>
    <id>https://liangji-seu.github.io/2026/03/25/ROS2-%E5%AE%8C%E6%95%B4%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%8D%97/</id>
    <published>2026-03-25T09:57:53.000Z</published>
    <updated>2026-06-14T15:43:50.966Z</updated>
    
    <content type="html"><![CDATA[<h1 id="🚀-ROS2-完整学习指南"><a href="#🚀-ROS2-完整学习指南" class="headerlink" title="🚀 ROS2 完整学习指南"></a>🚀 ROS2 完整学习指南</h1><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ol><li><a href="#%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8B">整体流程</a></li><li><a href="#%E8%AF%A6%E7%BB%86%E8%AE%B2%E8%A7%A3">详细讲解</a></li><li><a href="#%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E9%80%9F%E6%9F%A5">核心概念速查</a></li><li><a href="#%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E5%91%BD%E4%BB%A4">日常使用命令</a></li><li><a href="#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94">常见问题解答</a></li></ol><hr><h2 id="整体流程"><a href="#整体流程" class="headerlink" title="整体流程"></a>整体流程</h2><figure class="highlight scss"><table><tr><td class="code"><pre><span class="line">【第<span class="number">0</span>步】安装ROS2</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">1</span>步】创建工作空间 (workspace)</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">2</span>步】克隆源码到工作空间</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">3</span>步】安装依赖 (rosdep)</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">4</span>步】编译项目 (colcon build)</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">5</span>步】设置环境变量 (source setup.bash)</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">6</span>步】启动节点 (ros2 launch / ros2 run)</span><br><span class="line">    ↓</span><br><span class="line">【第<span class="number">7</span>步】与节点交互 (ros2 service call / ros2 topic echo)</span><br></pre></td></tr></table></figure><hr><h2 id="详细讲解"><a href="#详细讲解" class="headerlink" title="详细讲解"></a>详细讲解</h2><h3 id="【第0步】安装-ROS2"><a href="#【第0步】安装-ROS2" class="headerlink" title="【第0步】安装 ROS2"></a>【第0步】安装 ROS2</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 从官方网站或鱼香ROS安装</span></span><br><span class="line"><span class="comment"># 这会安装到 /opt/ros/humble (或其他版本)</span></span><br></pre></td></tr></table></figure><p><strong>安装后，ROS2给你提供了什么？</strong></p><ul><li>编译工具：<code>colcon</code></li><li>运行工具：<code>ros2</code> 命令行</li><li>基础库：<code>rclcpp</code>, <code>rclpy</code> 等</li><li>系统工具：<code>rosdep</code> (软件包管理器)</li></ul><hr><h3 id="【第1步】创建工作空间-Workspace"><a href="#【第1步】创建工作空间-Workspace" class="headerlink" title="【第1步】创建工作空间 (Workspace)"></a>【第1步】创建工作空间 (Workspace)</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p ~/xsens_ws/src</span><br><span class="line"><span class="built_in">cd</span> ~/xsens_ws</span><br></pre></td></tr></table></figure><p><strong>什么是工作空间？</strong></p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">~<span class="operator">/</span>xsens_ws<span class="symbol">/</span>                    ← 工作空间根目录</span><br><span class="line">├── src<span class="symbol">/</span>                       ← 源码目录（你的项目放这里）</span><br><span class="line">│   └── xsens_mtw_driver_ros2-main<span class="symbol">/</span></span><br><span class="line">│       ├── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">│       ├── interfaces<span class="symbol">/</span></span><br><span class="line">│       └── ...</span><br><span class="line">│</span><br><span class="line">├── build<span class="symbol">/</span>                     ← 构建目录（编译过程产生，可以删除）</span><br><span class="line">│   ├── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">│   └── ...</span><br><span class="line">│</span><br><span class="line">├── install<span class="symbol">/</span>                   ← 安装目录（可执行文件、库等）</span><br><span class="line">│   ├── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">│   ├── setup.bash            ← ⭐ 最重要的文件</span><br><span class="line">│   └── ...</span><br><span class="line">│</span><br><span class="line">└── log<span class="symbol">/</span>                       ← 日志目录</span><br></pre></td></tr></table></figure><p><strong>为什么要创建工作空间？</strong></p><ul><li>隔离项目：不同项目用不同的工作空间</li><li>方便管理：编译、安装、运行都在这个目录下</li><li>环境隔离：一个工作空间的环境不会污染另一个</li></ul><hr><h3 id="【第2步】克隆源码"><a href="#【第2步】克隆源码" class="headerlink" title="【第2步】克隆源码"></a>【第2步】克隆源码</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/xsens_ws/src</span><br><span class="line">git <span class="built_in">clone</span> &lt;仓库地址&gt;</span><br></pre></td></tr></table></figure><p><strong>现在你的目录结构是：</strong></p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">~<span class="operator">/</span>xsens_ws<span class="operator">/</span>src<span class="symbol">/</span></span><br><span class="line">└── xsens_mtw_driver_ros2-main<span class="symbol">/</span></span><br><span class="line">    ├── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">    ├── interfaces<span class="symbol">/</span></span><br><span class="line">    ├── README.md</span><br><span class="line">    └── CMakeLists.txt</span><br></pre></td></tr></table></figure><hr><h3 id="【第3步】安装依赖-rosdep"><a href="#【第3步】安装依赖-rosdep" class="headerlink" title="【第3步】安装依赖 (rosdep)"></a>【第3步】安装依赖 (rosdep)</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/xsens_ws</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始化rosdep（只需第一次）</span></span><br><span class="line"><span class="built_in">sudo</span> rosdep init</span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新rosdep数据库</span></span><br><span class="line">rosdep update</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装所有依赖</span></span><br><span class="line">rosdep install --from-paths src --ignore-src -r -y</span><br></pre></td></tr></table></figure><p><strong>这些命令做什么？</strong></p><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>rosdep init</code></td><td>初始化rosdep系统</td></tr><tr><td><code>rosdep update</code></td><td>从网络更新依赖包列表</td></tr><tr><td><code>rosdep install</code></td><td>自动安装所有依赖</td></tr></tbody></table><p><strong>具体来说：</strong></p><p><code>rosdep install</code> 会：</p><ol><li>读取所有 <code>package.xml</code> 文件</li><li>找出声明的依赖（<code>&lt;depend&gt;</code>, <code>&lt;build_depend&gt;</code> 等）</li><li>用系统包管理器（apt）安装这些依赖</li></ol><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="comment">&lt;!-- xsens_mtw_driver/package.xml --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">package</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">name</span>&gt;</span>xsens_mtw_driver<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">build_depend</span>&gt;</span>rclcpp<span class="tag">&lt;/<span class="name">build_depend</span>&gt;</span>      ← rosdep会安装这些</span><br><span class="line">  <span class="tag">&lt;<span class="name">build_depend</span>&gt;</span>tf2_ros<span class="tag">&lt;/<span class="name">build_depend</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">build_depend</span>&gt;</span>geometry_msgs<span class="tag">&lt;/<span class="name">build_depend</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">exec_depend</span>&gt;</span>rclcpp<span class="tag">&lt;/<span class="name">exec_depend</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">package</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>如果没有 <code>rosdep install</code>：</strong></p><ul><li>编译时会报错：<code>找不到 rclcpp/rclcpp.hpp</code></li><li>因为依赖的库没装</li></ul><hr><h3 id="【第4步】编译项目-colcon-build"><a href="#【第4步】编译项目-colcon-build" class="headerlink" title="【第4步】编译项目 (colcon build)"></a>【第4步】编译项目 (colcon build)</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/xsens_ws</span><br><span class="line">colcon build --symlink-install</span><br></pre></td></tr></table></figure><p><strong><code>colcon</code> 是什么？</strong></p><p>ROS2的编译工具，取代了ROS1的 <code>catkin_make</code>。</p><p><strong>编译流程：</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">CMakeLists.txt (构建配置)</span><br><span class="line">    ↓</span><br><span class="line">colcon 读取</span><br><span class="line">    ↓</span><br><span class="line">    ├─ 找到所有package</span><br><span class="line">    ├─ 按依赖顺序编译</span><br><span class="line">    │  (interfaces先编译，因为其他包依赖它)</span><br><span class="line">    ├─ 对每个package运行cmake</span><br><span class="line">    └─ 生成可执行文件、库等</span><br><span class="line">    ↓</span><br><span class="line">build/ 目录</span><br><span class="line">    ├─ xsens_mtw_driver/build文件</span><br><span class="line">    ├─ interfaces/build文件</span><br><span class="line">    └─ ...</span><br><span class="line">    ↓</span><br><span class="line">install/ 目录</span><br><span class="line">    ├─ lib/xsens_mtw_driver/xsens_mtw_manager (可执行文件)</span><br><span class="line">    ├─ lib/libxsensdeviceapi.so (库文件)</span><br><span class="line">    ├─ setup.bash (环境脚本)</span><br><span class="line">    └─ ...</span><br></pre></td></tr></table></figure><p><strong><code>--symlink-install</code> 是什么？</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">正常编译：</span><br><span class="line">install/ 中是复制的文件，修改代码需要重新编译</span><br><span class="line"></span><br><span class="line">--symlink-install：</span><br><span class="line">install/ 中是符号链接，指向 build/ 中的文件</span><br><span class="line">修改Python代码后，不需要重新编译，直接生效</span><br><span class="line"></span><br><span class="line">但对于C++代码，还是需要重新编译</span><br></pre></td></tr></table></figure><p><strong>编译后，install 目录里有什么？</strong></p><figure class="highlight nix"><table><tr><td class="code"><pre><span class="line">install<span class="symbol">/</span></span><br><span class="line">├── bin<span class="symbol">/</span>                        (可执行文件)</span><br><span class="line">├── lib<span class="symbol">/</span>                        (库文件，包括.so)</span><br><span class="line">│   ├── libxsensdeviceapi.so   ⭐</span><br><span class="line">│   ├── libxstypes.so          ⭐</span><br><span class="line">│   └── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">│       ├── xsens_mtw_manager</span><br><span class="line">│       └── xsens_mtw_visualization</span><br><span class="line">├── share<span class="symbol">/</span>                      (数据文件)</span><br><span class="line">│   └── xsens_mtw_driver<span class="symbol">/</span></span><br><span class="line">│       ├── launch<span class="symbol">/</span></span><br><span class="line">│       │   ├── xsens_mtw_manager.launch.py</span><br><span class="line">│       │   └── xsens_mtw_visualization.launch.py</span><br><span class="line">│       └── config<span class="symbol">/</span></span><br><span class="line">│           └── params.yaml</span><br><span class="line">└── setup.bash                  ⭐ 最重要！</span><br></pre></td></tr></table></figure><hr><h3 id="【第5步】设置环境变量-source-setup-bash"><a href="#【第5步】设置环境变量-source-setup-bash" class="headerlink" title="【第5步】设置环境变量 (source setup.bash)"></a>【第5步】设置环境变量 (source setup.bash)</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">source</span> ~/xsens_ws/install/setup.bash</span><br></pre></td></tr></table></figure><p><strong>这一步很关键！做什么的？</strong></p><p>打开 <code>setup.bash</code> 看看：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">cat</span> install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment">#!/bin/bash</span></span><br><span class="line"><span class="comment"># 设置 CMAKE_PREFIX_PATH</span></span><br><span class="line"><span class="built_in">export</span> CMAKE_PREFIX_PATH=<span class="string">&quot;/opt/ros/humble:~/xsens_ws/install:<span class="variable">$CMAKE_PREFIX_PATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置 PYTHONPATH</span></span><br><span class="line"><span class="built_in">export</span> PYTHONPATH=<span class="string">&quot;~/xsens_ws/install/lib/python3.10/site-packages:<span class="variable">$PYTHONPATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置 LD_LIBRARY_PATH (重要！)</span></span><br><span class="line"><span class="built_in">export</span> LD_LIBRARY_PATH=<span class="string">&quot;~/xsens_ws/install/lib:<span class="variable">$LD_LIBRARY_PATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置 PATH</span></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="string">&quot;~/xsens_ws/install/bin:<span class="variable">$PATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 其他环境变量...</span></span><br></pre></td></tr></table></figure><p><strong>关键的4个环境变量：</strong></p><table><thead><tr><th>变量</th><th>作用</th></tr></thead><tbody><tr><td><strong>LD_LIBRARY_PATH</strong></td><td>告诉OS在哪里找.so库文件</td></tr><tr><td><strong>PATH</strong></td><td>告诉OS在哪里找可执行文件</td></tr><tr><td><strong>PYTHONPATH</strong></td><td>告诉Python在哪里找模块</td></tr><tr><td><strong>CMAKE_PREFIX_PATH</strong></td><td>告诉CMake在哪里找包</td></tr></tbody></table><p><strong>如果不 <code>source setup.bash</code>：</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ ros2 run xsens_mtw_driver xsens_mtw_manager</span><br><span class="line">Command <span class="string">&#x27;xsens_mtw_manager&#x27;</span> not found</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">error <span class="keyword">while</span> loading shared libraries: libxsensdeviceapi.so:</span><br><span class="line">  cannot open shared object file</span><br></pre></td></tr></table></figure><hr><h3 id="【第6步】启动节点"><a href="#【第6步】启动节点" class="headerlink" title="【第6步】启动节点"></a>【第6步】启动节点</h3><h4 id="方式1：用-launch-脚本（推荐）"><a href="#方式1：用-launch-脚本（推荐）" class="headerlink" title="方式1：用 launch 脚本（推荐）"></a>方式1：用 launch 脚本（推荐）</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ros2 launch xsens_mtw_driver xsens_mtw_manager.launch.py</span><br></pre></td></tr></table></figure><p><strong>Launch 脚本做什么？</strong></p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># xsens_mtw_manager.launch.py</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">generate_launch_description</span>():</span><br><span class="line">    <span class="comment"># 1. 找到params.yaml文件</span></span><br><span class="line">    ros_config_params = get_package_share_path(...) / <span class="string">&#x27;config/params.yaml&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 2. 创建一个节点配置</span></span><br><span class="line">    node = Node(</span><br><span class="line">        package=<span class="string">&#x27;xsens_mtw_driver&#x27;</span>,</span><br><span class="line">        executable=<span class="string">&#x27;xsens_mtw_manager&#x27;</span>,</span><br><span class="line">        parameters=[ros_config_params],  <span class="comment"># 加载参数文件</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 3. 返回启动描述</span></span><br><span class="line">    <span class="keyword">return</span> LaunchDescription([node])</span><br></pre></td></tr></table></figure><p><strong>Launch 脚本的好处：</strong></p><ul><li>✅ 一条命令启动多个节点</li><li>✅ 自动加载参数文件</li><li>✅ 可以编程控制启动流程</li><li>✅ 可以设置重定向、命名空间等</li></ul><hr><h4 id="方式2：直接运行（不推荐）"><a href="#方式2：直接运行（不推荐）" class="headerlink" title="方式2：直接运行（不推荐）"></a>方式2：直接运行（不推荐）</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ros2 run xsens_mtw_driver xsens_mtw_manager</span><br></pre></td></tr></table></figure><p><strong>区别：</strong></p><ul><li>❌ 没有参数加载</li><li>❌ 需要手动指定所有参数</li><li>✅ 键盘输入可以工作</li></ul><hr><h3 id="【第7步】与节点交互"><a href="#【第7步】与节点交互" class="headerlink" title="【第7步】与节点交互"></a>【第7步】与节点交互</h3><h4 id="①-查看节点信息"><a href="#①-查看节点信息" class="headerlink" title="① 查看节点信息"></a>① 查看节点信息</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 列出所有正在运行的节点</span></span><br><span class="line">ros2 node list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">/xsens_mtw_manager</span><br><span class="line">/xsens_mtw_visualization</span><br></pre></td></tr></table></figure><hr><h4 id="②-查看话题（Topic）"><a href="#②-查看话题（Topic）" class="headerlink" title="② 查看话题（Topic）"></a>② 查看话题（Topic）</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 列出所有话题</span></span><br><span class="line">ros2 topic list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">/xsens_imu_data</span><br><span class="line">/tf</span><br><span class="line">/xsens_sync</span><br><span class="line">/rosout</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某个话题的数据</span></span><br><span class="line">ros2 topic <span class="built_in">echo</span> /xsens_imu_data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">imu_data:</span><br><span class="line">- header:</span><br><span class="line">    <span class="built_in">seq</span>: 42</span><br><span class="line">    stamp:</span><br><span class="line">      sec: 1234567890</span><br><span class="line">      nsec: 123456789</span><br><span class="line">  orientation:</span><br><span class="line">    w: 0.707</span><br><span class="line">    x: 0.0</span><br><span class="line">    y: 0.0</span><br><span class="line">    z: 0.707</span><br><span class="line">---</span><br></pre></td></tr></table></figure><hr><h4 id="③-调用服务（Service）⭐"><a href="#③-调用服务（Service）⭐" class="headerlink" title="③ 调用服务（Service）⭐"></a>③ 调用服务（Service）⭐</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 列出所有服务</span></span><br><span class="line">ros2 service list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">/xsens_mtw_manager/get_ready</span><br><span class="line">/xsens_mtw_manager/start_recording</span><br><span class="line">/xsens_mtw_manager/stop_recording</span><br><span class="line">/xsens_mtw_manager/imu_reset</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看服务的参数格式</span></span><br><span class="line">ros2 service <span class="built_in">type</span> /xsens_mtw_manager/get_ready</span><br><span class="line"><span class="comment"># 输出：xsens_srvs/srv/Trigger</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 调用服务</span></span><br><span class="line">ros2 service call /xsens_mtw_manager/get_ready xsens_srvs/srv/Trigger <span class="string">&quot;&#123;&#125;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">requester: making request: Trigger_Request()</span><br><span class="line"></span><br><span class="line">response:</span><br><span class="line">  success: <span class="literal">true</span></span><br><span class="line">  message: <span class="string">&quot;Ready!&quot;</span></span><br></pre></td></tr></table></figure><p><strong><code>ros2 service call</code> 的工作流程：</strong></p><ol><li>根据服务名找到对应的节点</li><li>根据服务类型 (<code>xsens_srvs/srv/Trigger</code>) 知道参数格式</li><li>发送请求给节点</li><li>等待节点返回响应</li></ol><hr><h2 id="核心概念速查"><a href="#核心概念速查" class="headerlink" title="核心概念速查"></a>核心概念速查</h2><table><thead><tr><th>概念</th><th>是什么</th><th>例子</th></tr></thead><tbody><tr><td><strong>工作空间</strong></td><td>项目的根目录</td><td>~&#x2F;xsens_ws&#x2F;</td></tr><tr><td><strong>package</strong></td><td>ROS中的一个项目单位</td><td>xsens_mtw_driver</td></tr><tr><td><strong>节点</strong></td><td>独立运行的程序</td><td>xsens_mtw_manager</td></tr><tr><td><strong>话题</strong></td><td>节点间的单向通信</td><td>&#x2F;xsens_imu_data</td></tr><tr><td><strong>服务</strong></td><td>节点间的请求-响应通信</td><td>&#x2F;xsens_mtw_manager&#x2F;get_ready</td></tr><tr><td><strong>参数</strong></td><td>配置信息</td><td>ros2_rate: 100</td></tr><tr><td><strong>rosdep</strong></td><td>依赖安装工具</td><td><code>rosdep install</code></td></tr><tr><td><strong>colcon</strong></td><td>编译工具</td><td><code>colcon build</code></td></tr><tr><td><strong>launch</strong></td><td>启动脚本</td><td>xsens_mtw_manager.launch.py</td></tr></tbody></table><hr><h2 id="日常使用命令"><a href="#日常使用命令" class="headerlink" title="日常使用命令"></a>日常使用命令</h2><h3 id="编译和安装阶段"><a href="#编译和安装阶段" class="headerlink" title="编译和安装阶段"></a>编译和安装阶段</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 只需做一次，或修改代码后</span></span><br><span class="line"><span class="built_in">cd</span> ~/xsens_ws</span><br><span class="line">colcon build --symlink-install</span><br><span class="line"><span class="built_in">source</span> install/setup.bash</span><br></pre></td></tr></table></figure><h3 id="运行阶段"><a href="#运行阶段" class="headerlink" title="运行阶段"></a>运行阶段</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 需要每次终端都做（或在 ~/.bashrc 中添加）</span></span><br><span class="line"><span class="built_in">source</span> ~/xsens_ws/install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动manager</span></span><br><span class="line">ros2 launch xsens_mtw_driver xsens_mtw_manager.launch.py</span><br><span class="line"></span><br><span class="line"><span class="comment"># 【另一个终端】</span></span><br><span class="line"><span class="built_in">source</span> ~/xsens_ws/install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动visualization</span></span><br><span class="line">ros2 launch xsens_mtw_driver xsens_mtw_visualization.launch.py</span><br><span class="line"></span><br><span class="line"><span class="comment"># 【第三个终端】</span></span><br><span class="line"><span class="built_in">source</span> ~/xsens_ws/install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment"># 调用服务</span></span><br><span class="line">ros2 service call /xsens_mtw_manager/get_ready xsens_srvs/srv/Trigger <span class="string">&quot;&#123;&#125;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看数据</span></span><br><span class="line">ros2 topic <span class="built_in">echo</span> /xsens_imu_data</span><br></pre></td></tr></table></figure><h3 id="信息查询命令"><a href="#信息查询命令" class="headerlink" title="信息查询命令"></a>信息查询命令</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 列出所有节点</span></span><br><span class="line">ros2 node list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某个节点的信息</span></span><br><span class="line">ros2 node info /xsens_mtw_manager</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出所有话题</span></span><br><span class="line">ros2 topic list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某个话题的详细信息</span></span><br><span class="line">ros2 topic info /xsens_imu_data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 实时查看话题数据</span></span><br><span class="line">ros2 topic <span class="built_in">echo</span> /xsens_imu_data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出所有服务</span></span><br><span class="line">ros2 service list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看服务类型</span></span><br><span class="line">ros2 service <span class="built_in">type</span> /xsens_mtw_manager/get_ready</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出所有参数</span></span><br><span class="line">ros2 param list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取某个参数值</span></span><br><span class="line">ros2 param get /xsens_mtw_manager ros2_rate</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置参数值</span></span><br><span class="line">ros2 param <span class="built_in">set</span> /xsens_mtw_manager ros2_rate 50</span><br></pre></td></tr></table></figure><hr><h2 id="常见问题解答"><a href="#常见问题解答" class="headerlink" title="常见问题解答"></a>常见问题解答</h2><h3 id="Q1-为什么每个终端都要-source-setup-bash？"><a href="#Q1-为什么每个终端都要-source-setup-bash？" class="headerlink" title="Q1: 为什么每个终端都要 source setup.bash？"></a>Q1: 为什么每个终端都要 <code>source setup.bash</code>？</h3><p><strong>A:</strong> 每个Bash shell都是独立的环境。<code>source setup.bash</code> 只对当前shell有效。新开一个终端时，环境变量被重置，需要重新 <code>source</code>。</p><p><strong>解决办法：</strong> 把 <code>source</code> 命令加到 <code>~/.bashrc</code>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;source ~/xsens_ws/install/setup.bash&quot;</span> &gt;&gt; ~/.bashrc</span><br></pre></td></tr></table></figure><p>这样每次打开终端都自动加载。</p><hr><h3 id="Q2-colcon-build-报错找不到依赖，怎么办？"><a href="#Q2-colcon-build-报错找不到依赖，怎么办？" class="headerlink" title="Q2: colcon build 报错找不到依赖，怎么办？"></a>Q2: <code>colcon build</code> 报错找不到依赖，怎么办？</h3><p><strong>A:</strong> 确保执行了 <code>rosdep install</code>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">rosdep install --from-paths src --ignore-src -r -y</span><br></pre></td></tr></table></figure><p>如果还是报错，可能需要：</p><ol><li>检查 <code>package.xml</code> 中是否正确声明了依赖</li><li>确保 <code>rosdep update</code> 已经执行</li><li>某些依赖可能需要手动安装</li></ol><hr><h3 id="Q3-修改了代码需要重新编译吗？"><a href="#Q3-修改了代码需要重新编译吗？" class="headerlink" title="Q3: 修改了代码需要重新编译吗？"></a>Q3: 修改了代码需要重新编译吗？</h3><p><strong>A:</strong></p><ul><li><strong>C++代码：</strong> 必须 <code>colcon build</code></li><li><strong>Python代码：</strong> 用 <code>--symlink-install</code> 的话，修改后直接生效</li><li><strong>YAML配置：</strong> 修改后直接生效（运行时加载）</li><li><strong>Launch文件：</strong> 修改后直接生效</li></ul><hr><h3 id="Q4-怎么知道哪个节点提供了哪些服务？"><a href="#Q4-怎么知道哪个节点提供了哪些服务？" class="headerlink" title="Q4: 怎么知道哪个节点提供了哪些服务？"></a>Q4: 怎么知道哪个节点提供了哪些服务？</h3><p><strong>A:</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ros2 service list                    <span class="comment"># 列出所有服务</span></span><br><span class="line"></span><br><span class="line">ros2 service <span class="built_in">type</span> /service_name       <span class="comment"># 查看服务类型</span></span><br><span class="line"></span><br><span class="line">ros2 service find xsens_srvs/srv/Trigger  <span class="comment"># 查找某类型的服务</span></span><br><span class="line"></span><br><span class="line">ros2 node info /xsens_mtw_manager    <span class="comment"># 查看节点提供的所有服务</span></span><br></pre></td></tr></table></figure><hr><h3 id="Q5-怎么给运行中的节点改参数？"><a href="#Q5-怎么给运行中的节点改参数？" class="headerlink" title="Q5: 怎么给运行中的节点改参数？"></a>Q5: 怎么给运行中的节点改参数？</h3><p><strong>A:</strong> 两种方法：</p><p><strong>方法1：编辑配置文件后重启节点</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 修改 xsens_mtw_driver/config/params.yaml</span></span><br><span class="line">ros2_rate: 50  <span class="comment"># 改成50Hz</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 重启</span></span><br><span class="line">colcon build</span><br><span class="line"><span class="built_in">source</span> install/setup.bash</span><br><span class="line">ros2 launch xsens_mtw_driver xsens_mtw_manager.launch.py</span><br></pre></td></tr></table></figure><p><strong>方法2：运行时修改</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ros2 param <span class="built_in">set</span> /xsens_mtw_manager ros2_rate 50</span><br></pre></td></tr></table></figure><hr><h3 id="Q6-怎么在多个工作空间之间切换？"><a href="#Q6-怎么在多个工作空间之间切换？" class="headerlink" title="Q6: 怎么在多个工作空间之间切换？"></a>Q6: 怎么在多个工作空间之间切换？</h3><p><strong>A:</strong> 每个工作空间都有自己的 <code>setup.bash</code>，只需 <code>source</code> 不同的文件即可：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 切换到ws1</span></span><br><span class="line"><span class="built_in">source</span> ~/ws1/install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment"># 切换到ws2</span></span><br><span class="line"><span class="built_in">source</span> ~/ws2/install/setup.bash</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看当前环境</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$ROS_PACKAGE_PATH</span></span><br></pre></td></tr></table></figure><p><strong>注意：</strong> 不要同时 <code>source</code> 多个工作空间，会导致环境变量冲突。</p><hr><h3 id="Q7-怎么调试ROS2节点？"><a href="#Q7-怎么调试ROS2节点？" class="headerlink" title="Q7: 怎么调试ROS2节点？"></a>Q7: 怎么调试ROS2节点？</h3><p><strong>A:</strong></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查看节点日志</span></span><br><span class="line">ros2 run xsens_mtw_driver xsens_mtw_manager --ros-args --log-level DEBUG</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 rqt_console 查看日志</span></span><br><span class="line">rqt_console</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 rosbag 录制数据</span></span><br><span class="line">ros2 bag record /xsens_imu_data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 播放录制的数据</span></span><br><span class="line">ros2 bag play &lt;bag_file&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="Q8-build-和-install-目录可以删除吗？"><a href="#Q8-build-和-install-目录可以删除吗？" class="headerlink" title="Q8: build 和 install 目录可以删除吗？"></a>Q8: build 和 install 目录可以删除吗？</h3><p><strong>A:</strong></p><ul><li><strong>可以删除：</strong> <code>build/</code> 和 <code>install/</code> 可以随时删除</li><li><strong>需要重新编译：</strong> 删除后需要再次 <code>colcon build</code></li><li><strong>环境失效：</strong> 删除 <code>install/</code> 后需要重新 <code>source setup.bash</code></li></ul><p>通常当编译出现问题时，可以尝试：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf build/ install/ <span class="built_in">log</span>/</span><br><span class="line">colcon build --symlink-install</span><br></pre></td></tr></table></figure><hr><h2 id="完整的工作流总结"><a href="#完整的工作流总结" class="headerlink" title="完整的工作流总结"></a>完整的工作流总结</h2><figure class="highlight nix"><table><tr><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 class="number">1</span>️⃣ 安装 ROS2</span><br><span class="line">   <span class="operator">/</span>opt<span class="operator">/</span>ros<span class="operator">/</span>humble<span class="symbol">/</span></span><br><span class="line">   ├── bin<span class="symbol">/</span>          (colcon, ros2等命令)</span><br><span class="line">   ├── lib<span class="symbol">/</span>          (ROS2的库文件)</span><br><span class="line">   └── share<span class="symbol">/</span>        (配置、消息定义等)</span><br><span class="line"></span><br><span class="line"><span class="number">2</span>️⃣ 创建工作空间</span><br><span class="line">   ~<span class="operator">/</span>xsens_ws<span class="symbol">/</span></span><br><span class="line">   ├── src<span class="symbol">/</span>          (你的源码)</span><br><span class="line">   └── install<span class="symbol">/</span>      (编译后的可执行文件和库)</span><br><span class="line"></span><br><span class="line"><span class="number">3</span>️⃣ 克隆项目</span><br><span class="line">   src<span class="operator">/</span>xsens_mtw_driver_ros2-main<span class="symbol">/</span></span><br><span class="line">   ├── CMakeLists.txt</span><br><span class="line">   ├── package.xml</span><br><span class="line">   └── 源代码...</span><br><span class="line"></span><br><span class="line"><span class="number">4</span>️⃣ 安装依赖 (rosdep install)</span><br><span class="line">   下载: rclcpp, tf2_ros, geometry_msgs 等</span><br><span class="line"></span><br><span class="line"><span class="number">5</span>️⃣ 编译 (colcon build)</span><br><span class="line">   编译C<span class="operator">++</span>代码生成可执行文件</span><br><span class="line">   复制<span class="operator">/</span>链接文件到 install<span class="symbol">/</span></span><br><span class="line"></span><br><span class="line"><span class="number">6</span>️⃣ 设置环境 (source setup.bash)</span><br><span class="line">   告诉OS在哪里找库和可执行文件</span><br><span class="line">   设置 LD_LIBRARY_PATH, PATH 等</span><br><span class="line"></span><br><span class="line"><span class="number">7</span>️⃣ 启动节点 (ros2 launch)</span><br><span class="line">   运行 xsens_mtw_manager 节点</span><br><span class="line">   节点开始发布话题、提供服务</span><br><span class="line"></span><br><span class="line"><span class="number">8</span>️⃣ 交互 (ros2 service call)</span><br><span class="line">   调用节点提供的服务</span><br><span class="line">   订阅节点发布的话题</span><br></pre></td></tr></table></figure><hr><h2 id="深入学习资源"><a href="#深入学习资源" class="headerlink" title="深入学习资源"></a>深入学习资源</h2><h3 id="官方文档"><a href="#官方文档" class="headerlink" title="官方文档"></a>官方文档</h3><ul><li><a href="https://docs.ros.org/en/humble/">ROS2官方文档</a></li><li><a href="https://docs.ros.org/en/humble/Tutorials.html">ROS2 Tutorials</a></li><li><a href="https://docs.ros.org/en/humble/Concepts.html">ROS2 Concepts</a></li></ul><h3 id="本地命令帮助"><a href="#本地命令帮助" class="headerlink" title="本地命令帮助"></a>本地命令帮助</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查看所有ros2子命令</span></span><br><span class="line">ros2 --<span class="built_in">help</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某个命令的帮助</span></span><br><span class="line">ros2 run --<span class="built_in">help</span></span><br><span class="line">ros2 launch --<span class="built_in">help</span></span><br><span class="line">ros2 service --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><hr><h2 id="关键要点总结"><a href="#关键要点总结" class="headerlink" title="关键要点总结"></a>关键要点总结</h2><p>✅ <strong>理解工作空间的目录结构</strong></p><ul><li>src&#x2F;: 源码</li><li>build&#x2F;: 编译产物</li><li>install&#x2F;: 安装产物</li></ul><p>✅ <strong>掌握编译流程</strong></p><ul><li>rosdep install: 安装依赖</li><li>colcon build: 编译代码</li><li>source setup.bash: 设置环境</li></ul><p>✅ <strong>了解ROS2的通信方式</strong></p><ul><li>话题 (Topic): 单向流式数据</li><li>服务 (Service): 请求-响应式通信</li><li>参数 (Parameter): 配置信息</li></ul><p>✅ <strong>学会与节点交互</strong></p><ul><li>ros2 run: 运行节点</li><li>ros2 launch: 用脚本启动</li><li>ros2 service call: 调用服务</li><li>ros2 topic echo: 查看数据</li></ul><p>✅ <strong>熟悉常用命令</strong></p><ul><li>ros2 node list</li><li>ros2 topic list</li><li>ros2 service list</li><li>ros2 param list</li></ul><hr><p><strong>现在你已经掌握了ROS2的基本概念和使用方法，可以开始深入学习和开发了！</strong> 🚀</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;🚀-ROS2-完整学习指南&quot;&gt;&lt;a href=&quot;#🚀-ROS2-完整学习指南&quot; class=&quot;headerlink&quot; title=&quot;🚀 ROS2 完整学习指南&quot;&gt;&lt;/a&gt;🚀 ROS2 完整学习指南&lt;/h1&gt;&lt;h2 id=&quot;目录&quot;&gt;&lt;a href=&quot;#目录&quot;</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="ROS" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/ROS/"/>
    
    
    <category term="ros" scheme="https://liangji-seu.github.io/tags/ros/"/>
    
    <category term="cpp" scheme="https://liangji-seu.github.io/tags/cpp/"/>
    
  </entry>
  
  <entry>
    <title>cache 学习笔记</title>
    <link href="https://liangji-seu.github.io/2026/03/24/cache-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <id>https://liangji-seu.github.io/2026/03/24/cache-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
    <published>2026-03-24T15:06:12.000Z</published>
    <updated>2026-10-03T10:34:31.630Z</updated>
    
    
    <summary type="html">记录 CPU cache、内存访问与操作系统相关的零散学习笔记。</summary>
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="OS" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/OS/"/>
    
    
    <category term="Cache" scheme="https://liangji-seu.github.io/tags/Cache/"/>
    
    <category term="内存" scheme="https://liangji-seu.github.io/tags/%E5%86%85%E5%AD%98/"/>
    
    <category term="OS" scheme="https://liangji-seu.github.io/tags/OS/"/>
    
  </entry>
  
  <entry>
    <title>vfs, 磁盘 讨论笔记</title>
    <link href="https://liangji-seu.github.io/2026/03/24/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0/"/>
    <id>https://liangji-seu.github.io/2026/03/24/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0/</id>
    <published>2026-03-24T15:05:58.000Z</published>
    <updated>2026-06-14T15:43:50.976Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#%E8%83%8C%E6%99%AF">背景</a></li><li><a href="#%E7%A3%81%E7%9B%98%E7%9A%84%E5%88%86%E7%B1%BB">磁盘的分类</a><ul><li><a href="#imx6ull-%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%A3%81%E7%9B%98">imx6ull 嵌入式系统中的磁盘</a></li><li><a href="#linux%E7%B3%BB%E7%BB%9F%E7%BB%84%E6%88%90">linux系统组成</a></li><li><a href="#linux%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B%E5%A4%8D%E8%BF%B0">linux系统启动流程复述</a></li><li><a href="#linux%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E8%BF%90%E8%A1%8C%E6%A8%A1%E5%9E%8B">linux系统内存运行模型</a></li><li><a href="#x86-ubuntu%E4%B8%AD%E7%9A%84%E7%A3%81%E7%9B%98">x86 ubuntu中的磁盘</a></li><li><a href="#%E6%9F%A5%E7%9C%8B">查看</a></li></ul></li><li><a href="#rootfs%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9Fvfs%E6%A6%82%E5%BF%B5%E6%A2%B3%E7%90%86">rootfs，文件系统，vfs概念梳理</a></li></ul><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我将自己的台式机配置成ubuntu服务器，我的台式机配置如下（按存储体系划分）：</p><ul><li>DDR 内存 32G</li><li>磁盘<ul><li>固态硬盘， 空闲176G， 被linux内核识别成 &#x2F;dev&#x2F;nvem1p1 这种SSD块设备</li><li>机械硬盘， 空闲390G， 被linux内核识别成 &#x2F;dev&#x2F;sdax 这种HDD块设备</li></ul></li></ul><p>借此机会，弄懂了VFS，rootfs，文件系统（ext4）, 内核启动根文件系统这些概念。</p><h1 id="磁盘的分类"><a href="#磁盘的分类" class="headerlink" title="磁盘的分类"></a>磁盘的分类</h1><p>CSAPP这本书里面讲计算机存储体系划分，DDR下面一层就是磁盘</p><p>但是我们实际的磁盘，是分为固态硬盘，和机械硬盘的，他们在种类上归属于磁盘，对应于开发板的EMMC（回想一下imx6ull, 在emmc里面进行分区）</p><h2 id="imx6ull-嵌入式系统中的磁盘"><a href="#imx6ull-嵌入式系统中的磁盘" class="headerlink" title="imx6ull 嵌入式系统中的磁盘"></a>imx6ull 嵌入式系统中的磁盘</h2><p>我们在imx6ull开发板中，磁盘是<code>emmc</code>，或者是<code>nand</code>, <code>sd卡</code> 这种flash.</p><p>实际在系统启动前，我们希望把<strong>一切需要启动的系统软件</strong>，全部放在<strong>磁盘&#x2F;FLASH</strong>中。</p><p>并且，我们希望在磁盘&#x2F;FLASH中存放的东西容易查找区分。所以，就会<strong>对磁盘进行分区</strong>，并对每个分区进行<strong>相应的格式化</strong>（<code>文件系统</code>，后面讲）</p><p>所以，我们在imx6ull的uboot中，可以用mmc 0:0 可以查看mmc flash设备的不同分区。然后发现，比如</p><ul><li><strong>emmc分区0</strong>，是<code>ext4</code>格式，存放rootfs根文件系统</li><li><strong>emmc分区1</strong>，<code>ext4</code>格式，存放 linux<strong>内核（kernel）的镜像</strong>（<code>zImage</code>）+设备树文件dtb.</li></ul><h2 id="linux系统组成"><a href="#linux系统组成" class="headerlink" title="linux系统组成"></a>linux系统组成</h2><p>这里先说一下linux系统。他并不是指的一个固件，而是指的一类系统。他的构成是：</p><ul><li><strong>uboot</strong> <ul><li>(bootloader, 当然有别的，用的最多的就是他)</li><li>系统引导程序，由他来初始化一些必要的硬件（DDR，串口等），为内核和系统启动创造环境</li></ul></li><li><strong>linux内核 + dtb设备树</strong><ul><li>这里只是内核，定义一个操作系统的<strong>底层功能</strong>：<ul><li>进程管理</li><li>内存管理</li><li>文件系统（VFS，后面讲）</li><li>网络</li><li>设备驱动</li><li><blockquote><p>如果是x86架构的ubuntu系统，是没有dtb设备树的。但是差别不算太大</p></blockquote></li></ul></li></ul></li><li><strong>根文件系统</strong><ul><li>根文件系统，就是<strong>一系列有目录结构的文件</strong>，定义了系统启动后要运行的各种服务。比如bash终端进程这些，不然你也没办法访问系统。</li></ul></li></ul><h2 id="linux系统启动流程复述"><a href="#linux系统启动流程复述" class="headerlink" title="linux系统启动流程复述"></a>linux系统启动流程复述</h2><p>所以，安装linux系统，肯定就是要了解启动流程：</p><ul><li><p>uboot启动，被加载到ddr中</p></li><li><p>uboot初始化必要的硬件，为内核启动创造环境</p></li><li><p>uboot自拷贝，腾出内存地方</p></li><li><p>uboot 去磁盘中的某一个分区，找到kernel镜像zImage, 还有dtb，拷贝到内存中</p><ul><li>记得我们的bootz 0x80000000 - 0x80000300吗</li></ul></li><li><p>uboot传递bootargs 和 bootcmd给kernel, 跳转内核入口，<strong>内核启动</strong>。</p></li><li><p>内核加载dtb，做一些初始化的操作，硬件这些。</p><ul><li>我们这个时候，就可以在终端看到打印的内核日志，启动各种设备</li></ul></li><li><p>内核启动各种功能，其中比较重要的就是VFS，虚拟文件系统。</p><ul><li>VFS，相当于为磁盘里面的文件，在内存里面创建映射的节点，其中最重要的就是 &#x2F; 根节点。</li><li>VFS，还在内存里面创建一些给内核用的文件夹，&#x2F;dev, &#x2F;tmp, &#x2F;sys</li><li>VFS, 为这些文件节点，提供统一的访问接口（read, write,….）</li></ul></li><li><p>内核开始挂载根文件系统。</p><ul><li>所谓挂载，就是相当于重映射，根文件系统，其内部结构也是从 &#x2F; 这个根节点开始延申的目录结构。挂载就是把 内存里面 VFS的&#x2F;这个根节点文件，和磁盘的rootfs的根节点绑定。</li><li>这样你访问内存的&#x2F;, 内核VFS就会自动帮你重定向去访问磁盘的rootfs.</li></ul></li><li><p>内核启动rootfs中的初始化脚本。</p></li><li><p>开启各种系统服务进程</p></li><li><p>系统启动完成。</p></li></ul><h2 id="linux系统内存运行模型"><a href="#linux系统内存运行模型" class="headerlink" title="linux系统内存运行模型"></a>linux系统内存运行模型</h2><p><img src="/images/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0-01-0325001325.png" alt="alt text"><br>上图展示了，当系统启动后，整个存储体系中的内容。</p><ul><li>kernel的程序<ul><li>肯定是在内存中</li></ul></li><li>rootfs的程序<ul><li>就是一堆程序文件，存放在磁盘里面</li></ul></li><li>准备执行的程序<ul><li>从磁盘中拷贝出来，拷贝到DDR中</li><li><blockquote><p>磁盘中的程序文件，内部仅包括.text, .rodata, .data, .bss这些必要的段。</p></blockquote></li><li><blockquote><p>当要执行时，CPU把这些文件从磁盘中拷贝到内存中，并分配固定的栈，来运行他。</p></blockquote></li></ul></li></ul><h2 id="x86-ubuntu中的磁盘"><a href="#x86-ubuntu中的磁盘" class="headerlink" title="x86 ubuntu中的磁盘"></a>x86 ubuntu中的磁盘</h2><p>在x86架构中，整体情况类似，我们肯定是要有地方存我们的系统的。我们肯定不希望断电，系统就没了，所以只能放在磁盘里面。对于一个抽象的计算机系统来说，这个系统肯定就是磁盘，无非就是放在固态硬盘，还是机械硬盘嘛。</p><p>前面，我们分析了，要在磁盘里面准备好系统，就是准备好：</p><ul><li>bootloader(BIOS)</li><li>rootfs(ubuntu)</li><li>linux内核</li></ul><p>所以理论上来说，无论你把这些文件，按分区放在机械硬盘，还是磁盘。理论上来说，<strong>都是可以正常启动</strong>。</p><p>但是实际上，虽然都能行，但是是有好坏的区分的。</p><blockquote><p>固态硬盘的速度远高于机械硬盘，但都是磁盘的速度范畴，肯定比不过ddr</p></blockquote><p>所以我们<strong>希望系统运行程序很快</strong>（因为rootfs里面是系统的各种程序，当系统运行时，执行各种程序，指令工具，都存放在里面，并且属于随机访问。所以，肯定希望拷贝速度越快越好。）</p><p>所以我们<strong>得出的结论</strong>是，把<strong>系统：ubuntu（rootfs）,linux内核，grub(bootloader)，全部装在固态硬盘这个磁盘里面</strong></p><p>这就是为什么，我们要在安装过程中，选择固态硬盘&#x2F;dev&#x2F;nvemop2这个固态硬盘的设备里面的空闲空间176G来作为&#x2F;根目录（rootfs）</p><p>把系统安装在固态硬盘里面。</p><p>然后在系统完全启动后，在&#x2F;data中，把机械硬盘的390G的空间（&#x2F;dev&#x2F;sda5）挂载到这个目录上去。这个磁盘存放数据。</p><p>为什么在这个磁盘存放数据，虽然他拷贝文件到内存的速度慢。但是我们是用来存东西的，拷贝机会少，而且放SDK这些源码，是拿到内存编译的，不是运行的（编译一个程序源文件的时间，远超从机械硬盘拷贝到内存的时间，所以忽略不计了）</p><p>所以这就决定好了我们安装ubuntu系统的磁盘使用。</p><h2 id="查看"><a href="#查看" class="headerlink" title="查看"></a>查看</h2><p>当进入系统后</p><ul><li><code>df -h</code><ul><li>可以查看文件系统的挂载情况</li><li><img src="/images/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0-02-0325001325.png" alt="alt text"></li><li><img src="/images/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0-03-0325001325.png" alt="alt text"></li><li><img src="/images/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0-04-0325001325.png" alt="alt text"></li></ul></li><li><code>lsblk</code><ul><li>可以查看磁盘的各个分区的磁盘容量，在里面可以看到固态磁盘是176G（其中有16G的swap分区，是用来给32G内存做虚拟拓展的，相当于一个cache缓存的空间）</li></ul></li></ul><h1 id="rootfs，文件系统，vfs概念梳理"><a href="#rootfs，文件系统，vfs概念梳理" class="headerlink" title="rootfs，文件系统，vfs概念梳理"></a>rootfs，文件系统，vfs概念梳理</h1><p>下面来梳理一下文件系统相关的概念</p><ul><li><strong>文件系统</strong><ul><li>ext4这些，其实是<strong>磁盘格式</strong>。用来格式化一个磁盘，把磁盘上无意义的存储单元，按一定格式规则组织起来，这样就可以表示文件了（比如这一块存储单元，表示一个txt），一个磁盘上，要有文件系统，才能存放文件，不然就只是一堆无意义的存储单元。</li></ul></li><li>根文件系统rootfs<ul><li>就是<strong>一堆文件+文件夹</strong>。存放在磁盘上，内核启动好后就要去执行这些文件里面的一些文件，开启各种服务程序。</li></ul></li><li>VFS 虚拟文件系统<ul><li>这个属于内核的一个主要功能，是linux一切皆文件的由来，是内核的一个模块</li><li>VFS在内存中，创建一个同样有目录结构的文件树结构。其中根节点就是 &#x2F;，</li><li>作用：<ul><li>创建根目录节点&#x2F;， 本质上是一个dentry + inode数据结构，存在于内核内存，不是磁盘文件</li><li>预定义一些核心虚拟目录： &#x2F;dev, &#x2F;proc, &#x2F;sys。 这些目录里的内容和结构，由内核直接维护，不依赖磁盘文件</li><li>为所有的文件，提供统一的文件操作接口（对磁盘的操作就是拷贝，读写，对设备文件的操作，会被路由到驱动去）</li></ul></li><li>此时的&#x2F;， 只是一个空入口，没有实际内容。</li><li><strong>挂载rootfs</strong><ul><li>内核通过启动参数bootargs里面的“root&#x3D;&#x2F;dev&#x2F;nvme01n1p1”找到有rootfs的磁盘分区，执行挂载</li><li><strong>所谓挂载，就是把VFS里面的&#x2F;节点，和磁盘里面的ext4文件系统rootfs的根目录，建立映射关系</strong>。</li><li>之后所有对&#x2F;，及其子目录的访问，都会由VFS转发给ext4驱动，去读写磁盘上的实际数据。</li></ul></li><li><blockquote><p>所以VFS，就是内核态的中间层。向上给用户态提供统一的文件操作接口，向下对接不同的实际文件系统</p></blockquote></li><li><blockquote><p>映射关系可以被覆盖，如果你在&#x2F;下创建data目录, 那么VFS中的&#x2F;data节点就映射到固态硬盘的磁盘里面。但是如果你此时把机械硬盘的文件系统（ext4）挂载到&#x2F;data上，这个映射关系就会被覆盖。此时&#x2F;data下的访问，就会被路由到机械硬盘上去。</p></blockquote></li></ul></li></ul><p><img src="/images/vfs-%E7%A3%81%E7%9B%98-%E8%AE%A8%E8%AE%BA%E7%AC%94%E8%AE%B0-05-0325001325.png" alt="alt text"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%A3%81%E7%9B%98%E7%9A%84%E5%88%86%E7%B1%BB&quot;&gt;磁盘的分类&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="OS" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/OS/"/>
    
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="OS" scheme="https://liangji-seu.github.io/tags/OS/"/>
    
    <category term="vfs" scheme="https://liangji-seu.github.io/tags/vfs/"/>
    
  </entry>
  
  <entry>
    <title>SDK中repo,git 笔记</title>
    <link href="https://liangji-seu.github.io/2026/03/24/SDK%E4%B8%ADrepo-git-%E7%AC%94%E8%AE%B0/"/>
    <id>https://liangji-seu.github.io/2026/03/24/SDK%E4%B8%ADrepo-git-%E7%AC%94%E8%AE%B0/</id>
    <published>2026-03-24T14:58:03.000Z</published>
    <updated>2026-06-14T15:43:50.966Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#repogit%E5%B5%8C%E5%85%A5%E5%BC%8Fsdk%E5%BC%80%E5%8F%91%E6%A0%B8%E5%BF%83%E7%AC%94%E8%AE%B0">Repo+Git嵌入式SDK开发核心笔记</a><ul><li><a href="#%E4%B8%80%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E7%81%B5%E9%AD%82">一、核心概念（灵魂）</a><ul><li><a href="#1-manifest-%E4%BB%93%E5%BA%93">1. Manifest 仓库</a></li><li><a href="#2-repo-%E5%B7%A5%E5%85%B7">2. Repo 工具</a></li><li><a href="#3-repo-sync--c">3. repo sync -c</a></li></ul></li><li><a href="#%E4%BA%8C%E5%88%86%E6%94%AF%E7%BB%93%E6%9E%84%E4%B8%8E%E5%85%B3%E7%B3%BB">二、分支结构与关系</a><ul><li><a href="#1-manifest-%E5%88%86%E6%94%AF%E7%89%88%E6%9C%AC%E5%85%A5%E5%8F%A3">1. Manifest 分支（版本入口）</a></li><li><a href="#2-%E5%AD%90%E4%BB%93%E5%BA%93%E5%88%86%E6%94%AF%E5%BC%80%E5%8F%91%E4%BD%BF%E7%94%A8">2. 子仓库分支（开发使用）</a></li></ul></li><li><a href="#%E4%B8%89git-%E6%A0%B8%E5%BF%83%E5%91%BD%E4%BB%A4%E5%BF%85%E8%83%8C">三、Git 核心命令（必背）</a><ul><li><a href="#1-git-fetch">1. git fetch</a></li><li><a href="#2-git-rebase-sdk-%E5%BC%80%E5%8F%91%E6%A0%B8%E5%BF%83">2. git rebase 【SDK 开发核心】</a></li><li><a href="#3-git-pull">3. git pull</a></li></ul></li><li><a href="#%E5%9B%9B%E9%AB%98%E9%A2%91%E7%96%91%E9%97%AE%E4%B8%8E%E7%9C%9F%E7%9B%B8%E9%81%BF%E5%9D%91%E5%BF%85%E5%A4%87">四、高频疑问与真相（避坑必备）</a><ul><li><a href="#1-%E4%B8%A4%E4%B8%AA%E5%88%86%E6%94%AF%E6%8F%90%E4%BA%A4%E4%B8%80%E6%A0%B7%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%80%E4%B8%AA%E8%83%BD-sync-%E4%B8%80%E4%B8%AA%E6%8A%A5%E9%94%99">1. 两个分支提交一样，为什么一个能 sync 一个报错？</a></li><li><a href="#2-repo-sync-%E5%88%B0%E5%BA%95%E4%BB%A5%E4%BB%80%E4%B9%88%E4%B8%BA%E5%87%86">2. repo sync 到底以什么为准？</a></li><li><a href="#3-%E4%B8%BA%E4%BB%80%E4%B9%88-repo-sync-%E5%90%8E%E6%98%AF-no-branch">3. 为什么 repo sync 后是 (no branch)？</a></li><li><a href="#4-default-%E5%88%86%E6%94%AF%E6%98%AF%E4%BB%80%E4%B9%88">4. default 分支是什么？</a></li><li><a href="#5-manifests-%E7%9B%AE%E5%BD%95%E7%9A%84%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E9%80%BB%E8%BE%91%E6%A0%B8%E5%BF%83%E7%96%91%E9%97%AE">5. manifests 目录的版本管理逻辑（核心疑问）</a></li></ul></li><li><a href="#%E4%BA%94sdk-%E5%BC%80%E5%8F%91%E6%A0%87%E5%87%86%E5%B7%A5%E4%BD%9C%E6%B5%81%E6%B0%B8%E4%B8%8D%E7%BF%BB%E8%BD%A6">五、SDK 开发标准工作流（永不翻车）</a><ul><li><a href="#1-%E5%90%8C%E6%AD%A5%E5%AE%98%E6%96%B9%E6%9C%80%E6%96%B0%E5%9F%BA%E7%BA%BF">1. 同步官方最新基线</a></li><li><a href="#2-%E5%88%9B%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF%E5%AE%89%E5%85%A8%E9%9A%94%E7%A6%BB">2. 创建自己的开发分支（安全隔离）</a></li><li><a href="#3-%E5%AE%98%E6%96%B9%E6%9B%B4%E6%96%B0%E5%90%8E%E5%88%B7%E6%96%B0%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%A0%81">3. 官方更新后，刷新你的代码</a></li></ul></li><li><a href="#%E5%85%AD%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%BB%88%E6%9E%81%E6%80%BB%E7%BB%93">六、一句话终极总结</a></li><li><a href="#%E4%B8%83%E9%81%BF%E5%9D%91%E9%93%81%E5%BE%8B">七、避坑铁律</a></li></ul></li></ul><h1 id="Repo-Git嵌入式SDK开发核心笔记"><a href="#Repo-Git嵌入式SDK开发核心笔记" class="headerlink" title="Repo+Git嵌入式SDK开发核心笔记"></a>Repo+Git嵌入式SDK开发核心笔记</h1><p>适用场景：RK3588 &#x2F; Linux &#x2F; Android 嵌入式 SDK 版本管理与开发</p><hr><h2 id="一、核心概念（灵魂）"><a href="#一、核心概念（灵魂）" class="headerlink" title="一、核心概念（灵魂）"></a>一、核心概念（灵魂）</h2><h3 id="1-Manifest-仓库"><a href="#1-Manifest-仓库" class="headerlink" title="1. Manifest 仓库"></a>1. Manifest 仓库</h3><ul><li><p><strong>路径</strong>：<code>.repo/manifests/</code></p></li><li><p><strong>定位</strong>：<strong>整个 SDK 的版本总控制器、版本清单</strong></p></li><li><p><strong>作用</strong>：通过 XML 文件定义：</p></li><li><p>每个子仓库（kernel、u-boot、device 等）</p></li><li><p>来自哪个远程</p></li><li><p>使用哪个分支 &#x2F;commit</p></li></ul><p><strong>本质</strong>：<strong>manifest 分支 &#x3D; 整套 SDK 的版本号</strong></p><p><strong>远程分支</strong>：厂商发布的<strong>稳定、可编译、官方版本</strong></p><p><strong>本地分支</strong>：你本地选中的版本入口</p><h3 id="2-Repo-工具"><a href="#2-Repo-工具" class="headerlink" title="2. Repo 工具"></a>2. Repo 工具</h3><ul><li><p>管理<strong>上百个 Git 子仓库</strong>的批量工具</p></li><li><p>不编写业务代码，<strong>只负责版本对齐</strong></p></li><li><p>所有子仓库的版本，<strong>完全由 manifest 决定</strong></p></li></ul><h3 id="3-repo-sync-c"><a href="#3-repo-sync-c" class="headerlink" title="3. repo sync -c"></a>3. repo sync -c</h3><ul><li><p><strong>作用</strong>：严格按照 manifest 清单，<strong>强制同步所有子仓库到官方指定版本</strong></p></li><li><p><strong>执行流程</strong>：</p></li><li><p>先同步 <code>.repo/manifests/</code> 自身到最新</p></li><li><p>读取当前分支下的 XML 配置</p></li><li><p>批量 checkout 所有子仓库到对应版本</p></li><li><p><strong>结果</strong>：子仓库变为 <code>(no branch)</code> 游离状态（正常）</p></li></ul><hr><h2 id="二、分支结构与关系"><a href="#二、分支结构与关系" class="headerlink" title="二、分支结构与关系"></a>二、分支结构与关系</h2><h3 id="1-Manifest-分支（版本入口）"><a href="#1-Manifest-分支（版本入口）" class="headerlink" title="1. Manifest 分支（版本入口）"></a>1. Manifest 分支（版本入口）</h3><ul><li><p>远程分支：<code>origin/linux</code> → 官方稳定版本</p></li><li><p>本地分支：</p></li><li><p><code>default</code>：跟踪 <code>origin/linux</code>，<strong>推荐使用、安全稳定</strong></p></li><li><p><code>linux</code>：本地分支，若上游配置错误会导致同步失败</p></li></ul><p><strong>铁律</strong>：</p><ul><li><p>切换 manifest 分支 &#x3D; <strong>切换整套 SDK 版本</strong></p></li><li><p>manifest 分支必须正确跟踪远程官方分支</p></li></ul><h3 id="2-子仓库分支（开发使用）"><a href="#2-子仓库分支（开发使用）" class="headerlink" title="2. 子仓库分支（开发使用）"></a>2. 子仓库分支（开发使用）</h3><ul><li><p>如 <code>kernel-5.10</code>、<code>u-boot</code> 内部分支</p></li><li><p><strong>repo sync 完全不看子仓库的本地分支</strong></p></li><li><p>子仓库会被 XML 配置<strong>强制覆盖版本</strong></p></li></ul><hr><h2 id="三、Git-核心命令（必背）"><a href="#三、Git-核心命令（必背）" class="headerlink" title="三、Git 核心命令（必背）"></a>三、Git 核心命令（必背）</h2><h3 id="1-git-fetch"><a href="#1-git-fetch" class="headerlink" title="1. git fetch"></a>1. git fetch</h3><ul><li><p><strong>作用</strong>：仅下载远程最新提交到<strong>本地缓存</strong></p></li><li><p>特点：</p></li><li><p>不修改本地代码</p></li><li><p>不合并、不覆盖、不切换分支</p></li></ul><p>生成：<code>origin/linux</code>（远程分支在本地的只读镜像）</p><h3 id="2-git-rebase-【SDK-开发核心】"><a href="#2-git-rebase-【SDK-开发核心】" class="headerlink" title="2. git rebase 【SDK 开发核心】"></a>2. git rebase 【SDK 开发核心】</h3><ul><li><p><strong>定义</strong>：变基 &#x3D; <strong>换底座</strong></p></li><li><p><strong>逻辑</strong>：</p></li><li><p>把本地提交从「旧基线」拆下</p></li><li><p>重新嫁接到「最新官方基线」之上</p></li></ul><p><strong>优点</strong>：</p><ul><li><p>历史线性干净</p></li><li><p>无多余合并提交</p></li><li><p>你的代码永远基于最新官方版本</p></li></ul><p><strong>禁忌</strong>：SDK 基线更新<strong>只用 rebase，不用 merge</strong></p><h3 id="3-git-pull"><a href="#3-git-pull" class="headerlink" title="3. git pull"></a>3. git pull</h3><ul><li><p>&#x3D; <code>git fetch + git merge</code></p></li><li><p><strong>不推荐在嵌入式 SDK 中使用</strong></p></li><li><p>会产生网状历史，难以维护、排查问题</p></li></ul><hr><h2 id="四、高频疑问与真相（避坑必备）"><a href="#四、高频疑问与真相（避坑必备）" class="headerlink" title="四、高频疑问与真相（避坑必备）"></a>四、高频疑问与真相（避坑必备）</h2><h3 id="1-两个分支提交一样，为什么一个能-sync-一个报错？"><a href="#1-两个分支提交一样，为什么一个能-sync-一个报错？" class="headerlink" title="1. 两个分支提交一样，为什么一个能 sync 一个报错？"></a>1. 两个分支提交一样，为什么一个能 sync 一个报错？</h3><ul><li><p>提交一样 &#x3D; 代码 &#x2F; XML 文件完全一致</p></li><li><p><strong>差异点</strong>：<strong>分支上游跟踪配置（upstream）</strong></p></li><li><p><code>repo sync</code> 会先同步 manifest 自身</p></li><li><p>若上游指向不存在的分支（如 <code>master</code>），直接报错</p></li></ul><h3 id="2-repo-sync-到底以什么为准？"><a href="#2-repo-sync-到底以什么为准？" class="headerlink" title="2. repo sync 到底以什么为准？"></a>2. repo sync 到底以什么为准？</h3><ul><li><p><strong><code>.repo/manifests</code></strong> <strong>以  当前分支为准</strong></p></li><li><p><strong>不以子仓库当前分支为准</strong></p></li><li><p>子仓库会被强制覆盖为 XML 中的版本</p></li></ul><h3 id="3-为什么-repo-sync-后是-no-branch-？"><a href="#3-为什么-repo-sync-后是-no-branch-？" class="headerlink" title="3. 为什么 repo sync 后是 (no branch)？"></a>3. 为什么 repo sync 后是 (no branch)？</h3><ul><li><p>因为 repo 直接切到一个 <strong>commit 哈希</strong></p></li><li><p>不是切到本地分支</p></li><li><p>属于<strong>正常、标准、预期</strong>状态</p></li></ul><h3 id="4-default-分支是什么？"><a href="#4-default-分支是什么？" class="headerlink" title="4. default 分支是什么？"></a>4. default 分支是什么？</h3><ul><li><p>本地分支，跟踪远程官方 <code>origin/linux</code></p></li><li><p><strong>官方版本在本地的镜像</strong></p></li><li><p>永远不要在 <code>default</code> 上开发写代码</p></li></ul><h3 id="5-manifests-目录的版本管理逻辑（核心疑问）"><a href="#5-manifests-目录的版本管理逻辑（核心疑问）" class="headerlink" title="5. manifests 目录的版本管理逻辑（核心疑问）"></a>5. manifests 目录的版本管理逻辑（核心疑问）</h3><ul><li><p><strong>核心原则</strong>：manifest 仓库的 <strong>远程分支 &#x3D; 官方 SDK 版本</strong>，但并非“一个改进一个新分支”，而是分「主版本分支」和「迭代更新」两种逻辑</p></li><li><p><strong>版本管理方式（官方常用）</strong>：</p><ul><li><p>① 主版本分支（长期存在）：比如 <code>origin/linux</code>（对应 SDK 主线版本）、<code>origin/linux-5.10</code>（对应 5.10 内核的稳定主线），这类分支会长期维护，用于承接日常小改进、bug 修复</p></li><li><p>② 里程碑版本（标签 tag 辅助）：官方发布重大版本（如 V1.0、V2.0）时，不会新建分支，而是在主分支（如 <code>origin/linux</code>）上打 <code>tag</code>（如 <code>v1.0.0</code>），标记该 commit 为“正式发布版本”，方便开发者回溯历史稳定版</p></li><li><p>③ 特殊版本分支（按需新建）：仅当出现“分支并行开发”需求时（如同时维护旧版 SDK 和新版 SDK），才会新建远程分支，比如 <code>origin/linux-legacy</code>（旧版维护）、<code>origin/linux-next</code>（新版测试）</p></li></ul></li><li><p><strong>官方新改进的推进逻辑</strong>：</p><ul><li><p>日常小改进、bug 修复、小版本迭代：<strong>不新建分支</strong>，直接在原有主分支（如 <code>origin/linux</code>）上提交更新，不断向前推进（commit 叠加）</p></li><li><p>重大版本升级（如内核从 5.10 升级到 6.1）：通常会新建分支（如 <code>origin/linux-6.1</code>），作为新的主版本分支，原有分支（<code>origin/linux-5.10</code>）保留，仅做 bug 修复，不再推进新功能</p></li><li><p>测试版、预览版：可能会新建临时分支（如 <code>origin/linux-dev</code>），测试稳定后，将代码合并到主分支，再删除临时分支</p></li></ul></li><li><p><strong>对开发者的影响</strong>：</p><ul><li><p>跟踪主分支（如 <code>origin/linux</code>）：执行 <code>git fetch + git rebase</code> 就能同步到官方最新改进，不用切换分支</p></li><li><p>需要固定版本：可以基于主分支的某个 tag（如 <code>v1.0.0</code>）新建本地分支，避免被官方更新覆盖</p></li><li><p>切换版本：若官方新建了新版本分支（如 <code>origin/linux-6.1</code>），只需修改 manifest 分支的跟踪关系，或切换 manifest 到对应分支，再执行 <code>repo sync -c</code> 即可同步整套新版本 SDK</p></li></ul></li></ul><hr><h2 id="五、SDK-开发标准工作流（永不翻车）"><a href="#五、SDK-开发标准工作流（永不翻车）" class="headerlink" title="五、SDK 开发标准工作流（永不翻车）"></a>五、SDK 开发标准工作流（永不翻车）</h2><h3 id="1-同步官方最新基线"><a href="#1-同步官方最新基线" class="headerlink" title="1. 同步官方最新基线"></a>1. 同步官方最新基线</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 切换到正确的 manifest 分支</span></span><br><span class="line"><span class="built_in">cd</span> .repo/manifests</span><br><span class="line">git checkout default</span><br><span class="line">git fetch origin</span><br><span class="line">git rebase origin/linux</span><br><span class="line"></span><br><span class="line"><span class="comment"># 同步整套 SDK</span></span><br><span class="line"><span class="built_in">cd</span> ../..</span><br><span class="line">.repo/repo/repo <span class="built_in">sync</span> -c -j1</span><br></pre></td></tr></table></figure><h3 id="2-创建自己的开发分支（安全隔离）"><a href="#2-创建自己的开发分支（安全隔离）" class="headerlink" title="2. 创建自己的开发分支（安全隔离）"></a>2. 创建自己的开发分支（安全隔离）</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 进入要开发的仓库</span></span><br><span class="line"><span class="built_in">cd</span> kernel-5.10</span><br><span class="line"><span class="comment"># 从官方基线新建开发分支</span></span><br><span class="line">git checkout -b develop</span><br></pre></td></tr></table></figure><h3 id="3-官方更新后，刷新你的代码"><a href="#3-官方更新后，刷新你的代码" class="headerlink" title="3. 官方更新后，刷新你的代码"></a>3. 官方更新后，刷新你的代码</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git fetch origin</span><br><span class="line">git rebase origin/linux</span><br></pre></td></tr></table></figure><hr><h2 id="六、一句话终极总结"><a href="#六、一句话终极总结" class="headerlink" title="六、一句话终极总结"></a>六、一句话终极总结</h2><ul><li><p><strong>Manifest &#x3D; 版本清单</strong></p></li><li><p><strong>Manifest 分支 &#x3D; 整套 SDK 版本</strong></p></li><li><p><strong>repo sync &#x3D; 按清单还原官方版本</strong></p></li><li><p><strong>git fetch &#x3D; 下载远程最新状态</strong></p></li><li><p><strong>git rebase &#x3D; 把你的代码搬家到最新官方基线</strong></p></li><li><p><strong>default 分支 &#x3D; 本地官方镜像，只用于同步，不用于开发</strong></p></li><li><p><strong>manifests 版本管理 &#x3D; 主分支承接日常迭代，tag 标记正式版本，特殊需求才新建分支</strong></p></li></ul><hr><h2 id="七、避坑铁律"><a href="#七、避坑铁律" class="headerlink" title="七、避坑铁律"></a>七、避坑铁律</h2><ol><li><p><strong>不要随意修改 manifest 分支</strong></p></li><li><p><strong>不要在 default 分支上写业务代码</strong></p></li><li><p><strong>repo sync 不会覆盖你的独立开发分支</strong></p></li><li><p><strong>上游配置错误是 manifest 同步失败的第一原因</strong></p></li><li><p><strong>SDK 基线更新永远用 rebase，不用 merge</strong></p></li><li><p><strong>不要随意切换 manifest 远程分支，避免同步到非预期的 SDK 版本</strong></p></li><li><p><strong>固定版本时，用 tag 新建本地分支，不要直接依赖主分支</strong></p><blockquote><p>（注：文档部分内容可能由 AI 生成）</p></blockquote></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#repogit%E5%B5%8C%E5%85%A5%E5%BC%8Fsdk%E5%BC%80%E5%8F%91%E6%A0%B8%E5%BF%83%E7%AC%94%E8%AE%B0&quot;&gt;Repo+Git嵌入式SDK开发核心笔记&lt;/a&gt;&lt;ul&gt;</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="LINUX" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/LINUX/"/>
    
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="SDK" scheme="https://liangji-seu.github.io/tags/SDK/"/>
    
    <category term="repo" scheme="https://liangji-seu.github.io/tags/repo/"/>
    
    <category term="git" scheme="https://liangji-seu.github.io/tags/git/"/>
    
    <category term="版本管理" scheme="https://liangji-seu.github.io/tags/%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>imx6ull-linux内核的misc驱动</title>
    <link href="https://liangji-seu.github.io/2026/03/10/imx6ull-linux%E5%86%85%E6%A0%B8%E7%9A%84misc%E9%A9%B1%E5%8A%A8/"/>
    <id>https://liangji-seu.github.io/2026/03/10/imx6ull-linux%E5%86%85%E6%A0%B8%E7%9A%84misc%E9%A9%B1%E5%8A%A8/</id>
    <published>2026-03-10T12:23:47.000Z</published>
    <updated>2026-06-14T15:43:50.969Z</updated>
    
    <content type="html"><![CDATA[<p>MISC 设备驱动笔记（MD格式）</p><h1 id="57-1-MISC-设备驱动简介"><a href="#57-1-MISC-设备驱动简介" class="headerlink" title="57.1 MISC 设备驱动简介"></a>57.1 MISC 设备驱动简介</h1><h2 id="图解"><a href="#图解" class="headerlink" title="图解"></a>图解</h2><p>原来我们cdev的写法，把前端创建用户空间的设备文件节点也放到设备结构体里面<br><img src="/images/imx6ull-linux%E5%86%85%E6%A0%B8%E7%9A%84misc%E9%A9%B1%E5%8A%A8-01-0310212247.png" alt="alt text"></p><p>现在用杂项，把这些完全剥离开来，设备结构体内存仅描述设备的特征属性，相当于开辟第三块内存，单独做用户空间的前端链路<br><img src="/images/imx6ull-linux%E5%86%85%E6%A0%B8%E7%9A%84misc%E9%A9%B1%E5%8A%A8-02-0310212247.png" alt="alt text"></p><h2 id="核心概念与优势"><a href="#核心概念与优势" class="headerlink" title="核心概念与优势"></a>核心概念与优势</h2><p>MISC 设备驱动是 Linux 字符设备驱动的简化方案，核心解决<strong>主设备号资源紧张</strong>的问题，所有 MISC 设备驱动的<strong>主设备号固定为 10</strong>，无需手动创建 cdev，<strong>大幅简化字符设备驱动的编写流程</strong>。</p><p><strong>就是原来我们编写一个led的驱动需要，在probe中需要：</strong></p><ul><li>初始化led设备结构体（后端链路：驱动-dts）<ul><li><blockquote><p><strong>开辟一块内存A，设置设备结构体</strong></p></blockquote></li><li>获取设备节点</li><li>申请gpio</li><li>xxx</li></ul></li><li>注册字符设备（前端链路：用户空间-设备文件节点-驱动）<ul><li><blockquote><p><strong>以前cdev这块内存(B∈A)是嵌在设备结构体的内存A里面的，cdev里面再指定操作方法集合（这个里面又操作设备结构体）</strong></p></blockquote></li><li>申请设备号</li><li>注册cdev</li><li>创建class,device</li></ul></li></ul><p>现在前端链路不需要做这么一大节了，只需要方便的注册misc设备（打包后的cdev设备）</p><ul><li>（前端链路）注册杂项设备<ul><li><blockquote><p><strong>也开辟一块内存C(C不属于A)，作为杂项设备，设置操作方法（内部来操作设备结构体的内存A）</strong></p></blockquote></li><li>misc_register(&amp;beep_miscdev)</li></ul></li></ul><blockquote><p>注意：</p><p>beep_miscdev是杂项设备，是struct miscdevice类型，他表示一个抽象的杂项设备。里面指定了他的操作方法，而这个操作方法里面，则是绑定我们真实的设备结构体（比如open方法里面，把我们的设备结构体加入到私有数据中）</p></blockquote><h2 id="miscdevice-结构体解析"><a href="#miscdevice-结构体解析" class="headerlink" title="miscdevice 结构体解析"></a>miscdevice 结构体解析</h2><p>MISC 设备通过 <code>miscdevice</code> 结构体描述，定义于 <code>include/linux/miscdevice.h</code>，核心成员及含义如下：</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">miscdevice</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> minor;                <span class="comment">/* 子设备号（核心需手动指定） */</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *name;         <span class="comment">/* 设备名（注册后在 /dev 生成对应设备文件） */</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">file_operations</span> *<span class="title">fops</span>;</span> <span class="comment">/* 设备操作集合（字符设备核心） */</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">list</span>;</span>    <span class="comment">/* 内核链表节点（内部使用） */</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device</span> *<span class="title">parent</span>;</span>    <span class="comment">/* 父设备（内部使用） */</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device</span> *<span class="title">this_device</span>;</span> <span class="comment">/* 设备结构体（内部使用） */</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">attribute_group</span> **<span class="title">groups</span>;</span> <span class="comment">/* 设备属性组（可选） */</span></span><br><span class="line"></span><br><span class="line">    <span class="type">umode_t</span> mode;             <span class="comment">/* 设备文件权限（可选） */</span></span><br><span class="line"></span><br><span class="line"> <span class="type">const</span> <span class="type">char</span> *nodename;     <span class="comment">/* 设备文件名（可选，默认与 name 一致） */</span></span><br><span class="line"></span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="关键成员设置"><a href="#关键成员设置" class="headerlink" title="关键成员设置"></a>关键成员设置</h3><p>定义 MISC 设备时，<strong>必须设置 <code>minor</code>、<code>name</code>、<code>fops</code> 三个核心成员</strong>：</p><ul><li><p><code>minor</code>：子设备号，主设备号固定为 10，需用户手动指定（需保证未被其他设备占用）；</p></li><li><p><code>name</code>：设备名称，注册成功后会在 <code>/dev</code> 目录生成同名设备文件；</p></li><li><p><code>fops</code>：字符设备操作集合（如 <code>open</code>&#x2F;<code>read</code>&#x2F;<code>write</code> 等函数实现），是 MISC 设备驱动的核心逻辑载体。</p></li></ul><h4 id="预定义子设备号"><a href="#预定义子设备号" class="headerlink" title="预定义子设备号"></a>预定义子设备号</h4><p>Linux 内核在 <code>include/linux/miscdevice.h</code> 中预定义了部分 MISC 子设备号（示例）：</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PSMOUSE_MINOR      1      <span class="comment">/* 鼠标设备 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MS_BUSMOUSE_MINOR  2      <span class="comment">/* 未使用 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ATARIMOUSE_MINOR   5      <span class="comment">/* 未使用 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SUN_MOUSE_MINOR    6      <span class="comment">/* 未使用 */</span></span></span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MISC_DYNAMIC_MINOR 255    <span class="comment">/* 动态子设备号 */</span></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="使用规则"><a href="#使用规则" class="headerlink" title="使用规则"></a>使用规则</h3><ul><li><p>可直接选用内核预定义的未占用子设备号；</p></li><li><p>也可自定义未被占用的子设备号，需保证唯一性。</p></li></ul><h2 id="MISC-设备注册与注销函数"><a href="#MISC-设备注册与注销函数" class="headerlink" title="MISC 设备注册与注销函数"></a>MISC 设备注册与注销函数</h2><h3 id="注册函数：misc-register"><a href="#注册函数：misc-register" class="headerlink" title="注册函数：misc_register"></a>注册函数：misc_register</h3><ul><li><p>函数原型：<code>int misc_register(struct miscdevice *misc)</code></p></li><li><p>功能：一次性完成字符设备驱动的<strong>申请设备号、初始化 cdev、添加 cdev、创建类、创建设备</strong>等所有步骤，替代传统繁琐的操作流程。</p></li><li><p>参数：<code>misc</code> - 待注册的 MISC 设备结构体指针；</p></li><li><p>返回值：<code>0</code> 成功，<strong>负数</strong> 失败。</p></li></ul><h3 id="注销函数：misc-deregister"><a href="#注销函数：misc-deregister" class="headerlink" title="注销函数：misc_deregister"></a>注销函数：misc_deregister</h3><ul><li><p>函数原型：<code>int misc_deregister(struct miscdevice *misc)</code></p></li><li><p>功能：一次性完成 MISC 设备的<strong>删除 cdev、注销设备号、删除设备、删除类</strong>等所有清理工作，简化卸载流程。</p></li><li><p>参数：<code>misc</code> - 待注销的 MISC 设备结构体指针；</p></li><li><p>返回值：<code>0</code> 成功，<strong>负数</strong> 失败。</p></li></ul><h2 id="传统驱动流程-vs-MISC-驱动流程"><a href="#传统驱动流程-vs-MISC-驱动流程" class="headerlink" title="传统驱动流程 vs MISC 驱动流程"></a>传统驱动流程 vs MISC 驱动流程</h2><h3 id="传统字符设备驱动流程（繁琐）"><a href="#传统字符设备驱动流程（繁琐）" class="headerlink" title="传统字符设备驱动流程（繁琐）"></a>传统字符设备驱动流程（繁琐）</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">alloc_chrdev_region();  <span class="comment">// 申请设备号</span></span><br><span class="line"></span><br><span class="line">cdev_init();            <span class="comment">// 初始化 cdev</span></span><br><span class="line"></span><br><span class="line">cdev_add();             <span class="comment">// 添加 cdev</span></span><br><span class="line"></span><br><span class="line">class_create();         <span class="comment">// 创建类</span></span><br><span class="line"></span><br><span class="line">device_create();        <span class="comment">// 创建设备</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="传统注销流程"><a href="#传统注销流程" class="headerlink" title="传统注销流程"></a>传统注销流程</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">cdev_del();             <span class="comment">// 删除 cdev</span></span><br><span class="line"></span><br><span class="line">unregister_chrdev_region(); <span class="comment">// 注销设备号</span></span><br><span class="line"></span><br><span class="line">device_destroy();       <span class="comment">// 删除设备</span></span><br><span class="line"></span><br><span class="line">class_destroy();        <span class="comment">// 删除类</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="MISC-驱动流程（极简）"><a href="#MISC-驱动流程（极简）" class="headerlink" title="MISC 驱动流程（极简）"></a>MISC 驱动流程（极简）</h3><ul><li><p>注册：仅需 <code>misc_register()</code> 完成所有初始化；</p></li><li><p>注销：仅需 <code>misc_deregister()</code> 完成所有清理。</p></li></ul><h2 id="后续应用"><a href="#后续应用" class="headerlink" title="后续应用"></a>后续应用</h2><p>基于 MISC 驱动框架，可结合 platform 驱动模型编写具体设备驱动（如 beep 蜂鸣器驱动），简化开发步骤。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/kernel.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/delay.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/ide.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/init.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/module.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/errno.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/gpio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/of_gpio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/miscdevice.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/platform_device.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BEEPOFF 0<span class="comment">/* 关蜂鸣器 */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BEEPON 1<span class="comment">/* 开蜂鸣器 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* miscbeep设备结构体 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">miscbeep_dev</span>&#123;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">device_node</span>*<span class="title">nd</span>;</span> <span class="comment">/* 设备节点 */</span></span><br><span class="line"><span class="type">int</span> beep_gpio;<span class="comment">/* beep所使用的GPIO编号*/</span></span><br><span class="line">&#125; miscbeep;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">miscbeep_open</span><span class="params">(<span class="keyword">struct</span> inode *inode, <span class="keyword">struct</span> file *filp)</span></span><br><span class="line">&#123;</span><br><span class="line">filp-&gt;private_data = &amp;miscbeep; <span class="comment">/* 设置私有数据 */</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">ssize_t</span> <span class="title function_">miscbeep_write</span><span class="params">(<span class="keyword">struct</span> file *filp, <span class="type">const</span> <span class="type">char</span> __user *buf, <span class="type">size_t</span> cnt, <span class="type">loff_t</span> *offt)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="type">int</span> retvalue;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> databuf[<span class="number">1</span>];</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> beepstat;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">miscbeep_dev</span> *<span class="title">dev</span> =</span> filp-&gt;private_data;</span><br><span class="line"></span><br><span class="line">retvalue = copy_from_user(databuf, buf, cnt);</span><br><span class="line"><span class="keyword">if</span>(retvalue &lt; <span class="number">0</span>) &#123;</span><br><span class="line">printk(<span class="string">&quot;kernel write failed!\r\n&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> -EFAULT;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">beepstat = databuf[<span class="number">0</span>];<span class="comment">/* 获取状态值 */</span></span><br><span class="line"><span class="keyword">if</span>(beepstat == BEEPON) &#123;</span><br><span class="line">gpio_set_value(dev-&gt;beep_gpio, <span class="number">0</span>);<span class="comment">/* 打开蜂鸣器 */</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span>(beepstat == BEEPOFF) &#123;</span><br><span class="line">gpio_set_value(dev-&gt;beep_gpio, <span class="number">1</span>);<span class="comment">/* 关闭蜂鸣器 */</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="class"><span class="keyword">struct</span> <span class="title">file_operations</span> <span class="title">miscbeep_fops</span> =</span> &#123;</span><br><span class="line">.owner = THIS_MODULE,</span><br><span class="line">.open = miscbeep_open,</span><br><span class="line">.write = miscbeep_write,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MISCBEEP_NAME<span class="string">&quot;miscbeep&quot;</span><span class="comment">/* 名字 */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MISCBEEP_MINOR144<span class="comment">/* 子设备号 */</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="class"><span class="keyword">struct</span> <span class="title">miscdevice</span> <span class="title">beep_miscdev</span> =</span> &#123;</span><br><span class="line">.minor = MISCBEEP_MINOR,</span><br><span class="line">.name = MISCBEEP_NAME,</span><br><span class="line">.fops = &amp;miscbeep_fops</span><br><span class="line">&#125;;</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 class="type">static</span> <span class="type">int</span> <span class="title function_">miscbeep_probe</span><span class="params">(<span class="keyword">struct</span> platform_device *dev)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">//第一块内存，是设备结构体，最后会挂到struct device上面，使device变得特殊</span></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> 1</span></span><br><span class="line"></span><br><span class="line">printk(<span class="string">&quot;beep driver and device was matched!\r\n&quot;</span>);</span><br><span class="line"><span class="comment">/* 设置BEEP所使用的GPIO */</span></span><br><span class="line"><span class="comment">/* 1、获取设备节点：beep */</span></span><br><span class="line">miscbeep.nd = of_find_node_by_path(<span class="string">&quot;/mygpiobeep@0&quot;</span>);</span><br><span class="line"><span class="keyword">if</span>(miscbeep.nd == <span class="literal">NULL</span>) &#123;</span><br><span class="line">printk(<span class="string">&quot;beep node not find!\r\n&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> -EINVAL;</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">/* 2、 获取设备树中的gpio属性，得到BEEP所使用的BEEP编号 */</span></span><br><span class="line">miscbeep.beep_gpio = of_get_named_gpio(miscbeep.nd, <span class="string">&quot;beep-gpio&quot;</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">if</span>(miscbeep.beep_gpio &lt; <span class="number">0</span>) &#123;</span><br><span class="line">printk(<span class="string">&quot;can&#x27;t get beep-gpio&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> -EINVAL;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 3、设置GPIO5_IO01为输出，并且输出高电平，默认关闭BEEP */</span></span><br><span class="line">ret = gpio_direction_output(miscbeep.beep_gpio, <span class="number">1</span>);</span><br><span class="line"><span class="keyword">if</span>(ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">printk(<span class="string">&quot;can&#x27;t set gpio!\r\n&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//第二块内存，杂项misc（把原来设备结构体里面通用的cdev部分全部独立出来）</span></span><br><span class="line"><span class="comment">//作为用户层访问的接口，内部操作方法来操作具体设备结构体的内存。</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> 1</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 一般情况下会注册对应的字符设备，但是这里我们使用MISC设备</span></span><br><span class="line"><span class="comment">   * 所以我们不需要自己注册字符设备驱动，只需要注册misc设备驱动即可</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">ret = misc_register(&amp;beep_miscdev);</span><br><span class="line"><span class="keyword">if</span>(ret &lt; <span class="number">0</span>)&#123;</span><br><span class="line">printk(<span class="string">&quot;misc device register failed!\r\n&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> -EFAULT;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">miscbeep_remove</span><span class="params">(<span class="keyword">struct</span> platform_device *dev)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">/* 注销设备的时候关闭LED灯 */</span></span><br><span class="line">gpio_set_value(miscbeep.beep_gpio, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 注销misc设备 */</span></span><br><span class="line">misc_deregister(&amp;beep_miscdev);</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</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 class="type">static</span> <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">of_device_id</span> <span class="title">beep_of_match</span>[] =</span> &#123;</span><br><span class="line">     &#123; .compatible = <span class="string">&quot;my-misc-beep&quot;</span> &#125;,</span><br><span class="line">     &#123; <span class="comment">/* Sentinel */</span> &#125;</span><br><span class="line"> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="class"><span class="keyword">struct</span> <span class="title">platform_driver</span> <span class="title">beep_driver</span> =</span> &#123;</span><br><span class="line">     .driver     = &#123;</span><br><span class="line"> .name = <span class="string">&quot;my-misc-beep&quot;</span>,</span><br><span class="line">         .of_match_table = beep_of_match, <span class="comment">/* 设备树匹配表          */</span></span><br><span class="line">     &#125;,</span><br><span class="line">     .probe      = miscbeep_probe,</span><br><span class="line">     .remove     = miscbeep_remove,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> __init <span class="title function_">miscbeep_init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">return</span> platform_driver_register(&amp;beep_driver);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> __exit <span class="title function_">miscbeep_exit</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">platform_driver_unregister(&amp;beep_driver);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">module_init(miscbeep_init);</span><br><span class="line">module_exit(miscbeep_exit);</span><br><span class="line">MODULE_LICENSE(<span class="string">&quot;GPL&quot;</span>);</span><br><span class="line">MODULE_AUTHOR(<span class="string">&quot;zuozhongkai&quot;</span>);</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;MISC 设备驱动笔记（MD格式）&lt;/p&gt;
&lt;h1 id=&quot;57-1-MISC-设备驱动简介&quot;&gt;&lt;a href=&quot;#57-1-MISC-设备驱动简介&quot; class=&quot;headerlink&quot; title=&quot;57.1 MISC 设备驱动简介&quot;&gt;&lt;/a&gt;57.1 MISC 设备驱</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="LINUX" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/LINUX/"/>
    
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="linux" scheme="https://liangji-seu.github.io/tags/linux/"/>
    
    <category term="驱动" scheme="https://liangji-seu.github.io/tags/%E9%A9%B1%E5%8A%A8/"/>
    
  </entry>
  
  <entry>
    <title>platform设备，驱动梳理</title>
    <link href="https://liangji-seu.github.io/2026/03/09/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86/"/>
    <id>https://liangji-seu.github.io/2026/03/09/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86/</id>
    <published>2026-03-09T13:06:16.000Z</published>
    <updated>2026-06-14T15:43:50.972Z</updated>
    
    <content type="html"><![CDATA[<ul><li><a href="#platform-device-driver%E6%A2%B3%E7%90%86">platform device driver梳理</a><ul><li><a href="#%E7%BB%93%E6%9E%84%E4%BD%93%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F%E4%B8%80%E8%A7%88">结构体成员变量一览</a></li><li><a href="#%E5%86%85%E6%A0%B8%E4%B8%AD%E5%8F%98%E9%87%8F%E7%BC%A9%E5%86%99%E5%91%BD%E5%90%8D">内核中变量缩写命名</a></li><li><a href="#gpiod%E5%92%8C%E4%B9%8B%E5%89%8D%E7%9A%84gpio%E5%AD%90%E7%B3%BB%E7%BB%9F">gpiod和之前的gpio子系统</a></li><li><a href="#linux%E5%86%85%E6%A0%B8%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%E4%B8%80%E8%A7%88">linux内核目录结构一览</a></li><li><a href="#platform_bus%E6%98%AF%E5%94%AF%E4%B8%80%E7%9A%84%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">platform_bus是唯一的全局变量</a></li></ul></li><li><a href="#%E5%85%B3%E4%BA%8Esys-proc%E5%92%8C%E9%A9%B1%E5%8A%A8%E7%9A%84%E5%85%B3%E8%81%94">关于&#x2F;sys, &#x2F;proc和驱动的关联</a><ul><li><a href="#sys-proc%E6%98%AF%E4%BB%80%E4%B9%88">&#x2F;sys &#x2F;proc是什么</a></li><li><a href="#%E6%88%91%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A9%B1%E5%8A%A8%E4%B8%AD%E5%88%9B%E5%BB%BAsys%E4%B8%8B%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%9C%E8%A5%BF">我如何在驱动中创建&#x2F;sys下的一些东西</a></li><li><a href="#sysfs%E6%BA%90%E7%A0%81%E7%9B%AE%E5%BD%95">sysfs源码目录</a></li><li><a href="#%E7%96%91%E9%97%AEclassdevice%E6%98%AF%E4%B8%BA%E4%BA%86%E5%9C%A8sysfs%E9%87%8C%E9%9D%A2%E5%88%9B%E5%BB%BA%E8%8A%82%E7%82%B9%E9%82%A3%E4%B9%88%E7%94%B3%E8%AF%B7%E8%AE%BE%E5%A4%87%E5%8F%B7%E6%B3%A8%E5%86%8C%E8%AE%BE%E5%A4%87%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88">疑问：class,device是为了在sysfs里面创建节点，那么申请设备号，注册设备的作用是什么？</a><ul><li><a href="#%E7%8E%B0%E4%BB%A3%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%E7%9A%84%E4%B8%8D%E5%90%8C">现代驱动开发的不同</a></li></ul></li></ul></li></ul><h1 id="platform-device-driver梳理"><a href="#platform-device-driver梳理" class="headerlink" title="platform device driver梳理"></a>platform device driver梳理</h1><p>这边对linux内核里面的结构体定义做一个梳理：</p><ul><li>struct platform_device<ul><li>struct device</li></ul></li><li>struct platform_driver<ul><li>struct device_driver</li></ul></li></ul><blockquote><p><code>platform_xxx</code>的定义都在<code>include/linux/platform_device.h</code></p><p><code>device</code>, <code>device_driver</code>的定义都在<code>include/linux/device.h</code></p></blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-01-0309230347.png" alt="alt text"><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-02-0309230347.png" alt="alt text"></p><p><strong>Linux里面的继承和多态</strong><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-03-0309230347.png" alt="alt text"></p><h2 id="结构体成员变量一览"><a href="#结构体成员变量一览" class="headerlink" title="结构体成员变量一览"></a>结构体成员变量一览</h2><p><strong>设备</strong></p><p><strong>基类</strong>：<code>struct device</code></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">device</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device</span> *<span class="title">parent</span>;</span>              <span class="comment">// 父设备（通常是其所属的总线控制器）</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device_private</span> *<span class="title">p</span>;</span>           <span class="comment">// 【严禁触碰】内核私有数据，存储总线链表位置等</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">kobject</span> <span class="title">kobj</span>;</span>                <span class="comment">// 内核对象，负责引用计数和 sysfs 层级生成</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *init_name;              <span class="comment">// 初始名字</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">device_type</span> *<span class="title">type</span>;</span>     <span class="comment">// 设备类型</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">mutex</span> <span class="title">mutex</span>;</span>                 <span class="comment">//互斥锁，用来同步堆驱动程序的调用</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">bus_type</span> *<span class="title">bus</span>;</span>               <span class="comment">// 设备所属的总线（如 &amp;platform_bus_type）</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device_driver</span> *<span class="title">driver</span>;</span>       <span class="comment">// 指向当前已经绑定的驱动程序</span></span><br><span class="line"></span><br><span class="line">    <span class="type">void</span> *platform_data;                <span class="comment">// 老式驱动传参方式 platform特有数据，device core不能访问他）</span></span><br><span class="line">    <span class="type">void</span> *driver_data;                  <span class="comment">//驱动数据，用dev_set/get_drvdata获取，保存设备结构体这些</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">dev_pm_info</span> <span class="title">power</span>;</span>           <span class="comment">// 电源管理状态信息</span></span><br><span class="line">    </span><br><span class="line">    <span class="meta">#<span class="keyword">ifdef</span> CONFIG_OF</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device_node</span> *<span class="title">of_node</span>;</span>        <span class="comment">// 【核心】指向设备树(DTS)中的节点，OF函数全靠它</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">fwnode_handle</span> *<span class="title">fwnode</span>;</span>       <span class="comment">// 固件节点句柄（统一了 ACPI 和 DT）</span></span><br><span class="line">    <span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="type">dev_t</span> devt;                         <span class="comment">// 设备号（主设备号+次设备号）</span></span><br><span class="line">    <span class="type">spinlock_t</span> devres_lock;             <span class="comment">// 管理设备资源的自旋锁</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">devres_head</span>;</span>       <span class="comment">// 由 devm_ 系列函数管理的资源列表</span></span><br><span class="line"></span><br><span class="line">    <span class="type">void</span> (*release)(<span class="keyword">struct</span> device *dev); <span class="comment">// 当引用计数归零时释放资源的函数</span></span><br><span class="line">    <span class="comment">// ... 还有处理 DMA、IOMMU、Numa 节点等非常底层的成员</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="type">dev_t</span>  devt;                        <span class="comment">//设备号</span></span><br><span class="line">    u32    id;                          <span class="comment">//设备实例</span></span><br><span class="line">    <span class="comment">//....</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>区别platform_data, driver_data<br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-01-0310202041.png" alt="alt text"><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-02-0310202041.png" alt="alt text"></p><p><strong>子类</strong>：<code>struct platform_device</code></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">platform_device</span> &#123;</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span>  *name;                  <span class="comment">// 设备名，用于和驱动的 name 进行匹配</span></span><br><span class="line">    <span class="type">int</span>         id;                     <span class="comment">// 设备 ID，用于区分同名设备。若只有一个则设为 -1</span></span><br><span class="line">    <span class="type">bool</span>        id_auto;                <span class="comment">// 是否自动分配 ID</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device</span> <span class="title">dev</span>;</span>                  <span class="comment">// 【基类】最核心的成员，包含了设备树节点和父设备信息</span></span><br><span class="line">    u32         num_resources;          <span class="comment">// 资源数组的数量</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">resource</span> *<span class="title">resource</span>;</span>          <span class="comment">// 【资源】指向 MEM、IRQ、DMA 等硬件资源描述的数组</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">platform_device_id</span> *<span class="title">id_entry</span>;</span> <span class="comment">// 匹配成功后，指向匹配到的 ID 表项</span></span><br><span class="line">    <span class="type">char</span> *driver_override;              <span class="comment">/* 强制匹配特定驱动的名字 */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* MFD (多功能设备) 相关指针 */</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">mfd_cell</span> *<span class="title">mfd_cell</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 架构特定的扩展数据 */</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">pdev_archdata</span> <span class="title">archdata</span>;</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>驱动</strong></p><p><strong>基类</strong>：<code>struct device_driver</code></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">device_driver</span> &#123;</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *name;              <span class="comment">// 驱动程序的名字</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">bus_type</span> *<span class="title">bus</span>;</span>          <span class="comment">// 所属总线</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">module</span> *<span class="title">owner</span>;</span>          <span class="comment">// 模块属主，通常填 THIS_MODULE</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *mod_name;          <span class="comment">// 用于内置模块的名字</span></span><br><span class="line">    <span class="type">bool</span> suppress_bind_attrs;      <span class="comment">// 是否禁止通过 sysfs 手动绑定/解绑</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">#<span class="keyword">ifdef</span> CONFIG_OF</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">of_device_id</span> *<span class="title">of_match_table</span>;</span> <span class="comment">// 【核心】设备树匹配表</span></span><br><span class="line">    <span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 通用探测函数：platform 总线会先调它，它内部再转换成 platform_device 调你的 probe</span></span><br><span class="line">    <span class="type">int</span> (*probe) (<span class="keyword">struct</span> device *dev);</span><br><span class="line">    <span class="type">int</span> (*remove) (<span class="keyword">struct</span> device *dev);</span><br><span class="line">    <span class="type">void</span> (*shutdown) (<span class="keyword">struct</span> device *dev);</span><br><span class="line">    <span class="type">int</span> (*suspend) (<span class="keyword">struct</span> device *dev, <span class="type">pm_message_t</span> state);</span><br><span class="line">    <span class="type">int</span> (*resume) (<span class="keyword">struct</span> device *dev);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 现代电源管理框架</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">dev_pm_ops</span> *<span class="title">pm</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 内核维护的私有数据</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">driver_private</span> *<span class="title">p</span>;</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>子类</strong>：struct platform_driver</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">platform_driver</span> &#123;</span></span><br><span class="line">    <span class="comment">/* 当总线匹配成功、设备被添加时执行 */</span></span><br><span class="line">    <span class="type">int</span> (*probe)(<span class="keyword">struct</span> platform_device *);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 当驱动卸载或设备被拔出时执行 */</span></span><br><span class="line">    <span class="type">int</span> (*remove)(<span class="keyword">struct</span> platform_device *);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 系统关机时的回调 */</span></span><br><span class="line">    <span class="type">void</span> (*shutdown)(<span class="keyword">struct</span> platform_device *);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 电源管理：休眠回调 */</span></span><br><span class="line">    <span class="type">int</span> (*suspend)(<span class="keyword">struct</span> platform_device *, <span class="type">pm_message_t</span> state);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 电源管理：唤醒回调 */</span></span><br><span class="line">    <span class="type">int</span> (*resume)(<span class="keyword">struct</span> platform_device *);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 【基类】通用的驱动属性，包含了名字、匹配表和模块所有者 */</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">device_driver</span> <span class="title">driver</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* ID 匹配表，用于支持一个驱动对应多个不同名字的设备 */</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">platform_device_id</span> *<span class="title">id_table</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 防止延迟探测的标志 */</span></span><br><span class="line">    <span class="type">bool</span> prevent_deferred_probe;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="内核中变量缩写命名"><a href="#内核中变量缩写命名" class="headerlink" title="内核中变量缩写命名"></a>内核中变量缩写命名</h2><p>遵循一套<strong>非常严谨的命名规范</strong></p><p><strong>1. 核心对象缩写 (名词)</strong></p><p>这些缩写通常代表<strong>结构体的实例指针</strong>。</p><ul><li><code>dev</code> (Device): 指向 <code>struct device</code> 的指针。它是所有设备的基类。</li><li><code>pdev</code> (Platform Device): 指向 <code>struct platform_device</code> 的指针。</li><li><code>drv</code> (Driver): 指向 <code>struct device_driver</code> 的指针。</li><li><code>pdrv</code> (Platform Driver): 指向 <code>struct platform_driver</code> 的指针。</li><li><code>node</code> &#x2F; <code>np</code> (Node Pointer): 通常指 <code>struct device_node *</code>，即设备树中的一个节点指针。</li><li><code>priv</code> Private Data”（<strong>私有数据</strong><ul><li>内核的设备类型platform_device是通用的，所以只定义了设备的共性，但是你的驱动中，常常要<strong>记录一些特有信息</strong>，比如GPIO编号，动态申请的内存地址，当前设备的运行状态，互斥锁，自旋锁</li><li>我们驱动里面使用的platform_device变量都是通用的。所以我们只能自己在驱动中开辟一块内存，然后自己创建设备描述结构体。然后把他们绑定到我们创建使用的通用的platform_device上（里面的指针指向我们这一块内存）</li><li>你不能修改内核定义的全局结构体，<strong>只能拿一个私有数据的指针</strong>，<strong>指向你驱动里面的开辟的全局内存</strong>。</li><li><blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-04-0309230347.png" alt="alt text"></p></blockquote></li><li><blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-05-0309230347.png" alt="alt text"></p></blockquote></li><li><blockquote><p><strong>关于内核申请的内存，其实是在空闲列表里面申请的，这个xv6里面搞清楚了</strong></p></blockquote></li><li></li><li><blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-06-0309230347.png" alt="alt text"></p></blockquote></li></ul></li></ul><hr><p><strong>2. 资源管理前缀 (动词&#x2F;特性)</strong></p><p><code>devm_</code> (Device <strong>Managed</strong>)</p><p>这是 Linux 内核的“<strong>自动垃圾回收</strong>”机制。</p><ul><li><p><strong>含义</strong>：带有 <code>devm_</code> 前缀的函数（如 <code>devm_kzalloc</code>, <code>devm_gpio_request</code>）申请的资源，会挂在 <code>struct device</code> 内部的 <code>devres_head</code> 链表上。</p></li><li><p><strong>作用</strong>：当驱动卸载（执行 <code>remove</code>）或 <code>probe 失败</code>时，<strong>内核会自动释放这些资源</strong>。你不需要在 remove 函数里手动写一堆 kfree 或 gpio_free。</p></li></ul><hr><p><code>of</code>_ (<strong>Open Firmware</strong>)</p><ul><li><p><strong>含义</strong>：所有<strong>处理设备树</strong> (Device Tree) 相关的函数都以 <code>of_</code> 开头。</p></li><li><p><strong>例子</strong>：of_get_named_gpio（从设备树获取 GPIO 编号），of_property_read_u32（读取设备树里的数字属性）。</p></li></ul><hr><p><strong>3. 常见缩写</strong><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-07-0309230347.png" alt="alt text"></p><p>所以看到pdev-&gt;dev, 就是获取设备的父类对象</p><h2 id="gpiod和之前的gpio子系统"><a href="#gpiod和之前的gpio子系统" class="headerlink" title="gpiod和之前的gpio子系统"></a>gpiod和之前的gpio子系统</h2><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-03-0310202041.png" alt="alt text"><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-04-0310202041.png" alt="alt text"><br><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-05-0310202041.png" alt="alt text"></p><h2 id="linux内核目录结构一览"><a href="#linux内核目录结构一览" class="headerlink" title="linux内核目录结构一览"></a>linux内核目录结构一览</h2><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">linux-src/</span><br><span class="line">├── arch/                        # 体系结构相关代码 (如 arm64, riscv, x86)</span><br><span class="line">│   └── arm64/boot/dts/          # 【重点】这里存放了你经常修改的设备树文件 (.dts)</span><br><span class="line">│</span><br><span class="line">│</span><br><span class="line">├── drivers/                     # 驱动程序主体</span><br><span class="line">│   ├── base/                    # 【核心核心】Linux 设备模型 (LDM) 的底层实现</span><br><span class="line">│   │   ├── bus.c                # 总线注册、匹配逻辑</span><br><span class="line">│   │   ├── dd.c                 # Driver <span class="title function_">Bound</span> <span class="params">(驱动绑定核心逻辑)</span></span><br><span class="line">│   │   ├── driver.c             <span class="meta"># struct device_driver 的通用操作</span></span><br><span class="line">│   │   ├── core.c               <span class="meta"># struct device 的通用操作</span></span><br><span class="line">│   │   └── platform.c           # 【重点】platform_driver_register 等函数的定义就在这！</span><br><span class="line">│   ├── leds/                    # LED 子系统目录</span><br><span class="line">│   │   ├── led-core.c           # LED 核心层逻辑</span><br><span class="line">│   │   ├── led-class.c          # /sys/class/leds 的创建与管理</span><br><span class="line">│   │   └── leds-gpio.c          # 【教科书】基于 platform 框架的最简单标准 LED 驱动</span><br><span class="line">│   ├── gpio/                    # GPIO 子系统 <span class="params">(gpiolib)</span></span><br><span class="line">│   ├── platform/                # 特定于某些硬件平台的“增强型”驱动 <span class="params">(非框架本身)</span></span><br><span class="line">│   │   └── x86/                 # 例如 Thinkpad 快捷键、华硕笔记本特有的驱动</span><br><span class="line">│   └── of/                      # Open <span class="title function_">Firmware</span> <span class="params">(设备树)</span> 的核心解析逻辑</span><br><span class="line">│</span><br><span class="line">│</span><br><span class="line">├── include/                     # 头文件目录</span><br><span class="line">│   ├── linux/                   # 【重点】通用的内核头文件 <span class="params">(与平台无关)</span></span><br><span class="line">│   │   ├── platform_device.h    <span class="meta"># struct platform_device 定义和 API 声明</span></span><br><span class="line">│   │   ├── device.h             <span class="meta"># struct device 基类定义</span></span><br><span class="line">│   │   ├── of.h                 # 设备树操作 <span class="title function_">API</span> <span class="params">(of_ 开头的函数)</span></span><br><span class="line">│   │   ├── leds.h               # LED 子系统相关结构体</span><br><span class="line">│   │   └── mod_devicetable.h    # of_device_id 等匹配表的定义</span><br><span class="line">│   └── uapi/                    # User <span class="title function_">API</span> <span class="params">(给应用层调用的头文件)</span></span><br><span class="line">│</span><br><span class="line">│</span><br><span class="line">├── Documentation/               # 内核文档</span><br><span class="line">│   └── devicetree/              # 设备树绑定文档 <span class="params">(告诉你 DTS 节点该怎么写)</span></span><br><span class="line">│</span><br><span class="line">│</span><br><span class="line">└── kernel/                      # 内核核心调度、同步、定时器逻辑</span><br></pre></td></tr></table></figure><h2 id="platform-bus是唯一的全局变量"><a href="#platform-bus是唯一的全局变量" class="headerlink" title="platform_bus是唯一的全局变量"></a>platform_bus是唯一的全局变量</h2><p>在 Linux 内核中，<code>platform_bus_type</code> 确实是一个全局唯一的 <code>struct bus_type</code> 实例</p><p>根据上面的目录结构分类，就很容易找到他的全局变量创建在<code>drivers/base/platform.c</code>中</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">bus_type</span> <span class="title">platform_bus_type</span> =</span> &#123;</span><br><span class="line">    .name= <span class="string">&quot;platform&quot;</span>,</span><br><span class="line">    .dev_groups= platform_dev_groups,</span><br><span class="line">    .match= platform_match,      <span class="comment">// 【核心】决定设备和驱动能否“牵手”成功</span></span><br><span class="line">    .uevent= platform_uevent,</span><br><span class="line">    .pm    = &amp;platform_dev_pm_ops, <span class="comment">// 电源管理</span></span><br><span class="line">    .probe= platform_drv_probe,  <span class="comment">// 封装后的探测函数</span></span><br><span class="line">    .remove= platform_drv_remove,</span><br><span class="line">    .shutdown= platform_drv_shutdown,</span><br><span class="line">&#125;;</span><br><span class="line">EXPORT_SYMBOL_GPL(platform_bus_type); <span class="comment">// 导出符号，让其他模块也能访问</span></span><br></pre></td></tr></table></figure><h1 id="关于-sys-proc和驱动的关联"><a href="#关于-sys-proc和驱动的关联" class="headerlink" title="关于&#x2F;sys, &#x2F;proc和驱动的关联"></a>关于&#x2F;sys, &#x2F;proc和驱动的关联</h1><h2 id="sys-proc是什么"><a href="#sys-proc是什么" class="headerlink" title="&#x2F;sys &#x2F;proc是什么"></a>&#x2F;sys &#x2F;proc是什么</h2><p><code>/proc</code> 是<strong>历史遗留的“大杂烩”</strong>，而 <code>/sys</code> 是伴随 Linux 设备模型（L<strong>DM</strong>）诞生的“<strong>现代化设备博物馆</strong>”</p><p>这两个目录下面都是<strong>挂载的虚拟文件系统</strong></p><hr><p><code>/proc</code> (Process Information Pseudo-file System)</p><ul><li><p><strong>初衷</strong>：最早在 Unix 时代，<code>/proc</code> 仅仅是为了向用户空间导出<strong>进程（Process</strong>信息而创建的（比如现在的 &#x2F;proc&#x2F;1 就是 init 进程的状态）。</p></li><li><p><strong>演变</strong>（失控）：随着 Linux 发展，内核开发者发现从内核向用户层传递数据太麻烦了，而 &#x2F;proc 刚好是个现成的通道。于是，大家开始往里面<strong>塞各种与进程无关的东西</strong>：<code>内存信息</code>（meminfo）、<code>中断</code>（interrupts）、<code>命令行参数</code>（cmdline），甚至包括你提到的<code>设备树</code>（device-tree）。</p></li><li><p><strong>缺点</strong>：毫无结构可言，格式混乱。有些文件里面是一大坨文本，用户层写脚本去解析这些文本非常痛苦（比如用 awk&#x2F;grep 抓取特定字段）。</p></li></ul><p><strong><code>/proc</code> 的常见目录</strong></p><ul><li><code>/proc/&quot;pid&quot;/</code>：各个进程的状态（老本行）。</li><li><code>/proc/interrupts</code>：系统的<strong>中断统计</strong>。</li><li><code>/proc/device-tree/</code>：设备树节点（实际上在现代内核中，它通常是一个指向 <code>/sys/firmware/devicetree/base</code> 的符号链接）。</li></ul><hr><p><code>/sys</code> (Sysfs)</p><ul><li><p><strong>初衷</strong>：在 Linux 2.6 引入设备模型（L<strong>DM</strong>）时，内核鼻祖 Linus 和其他维护者受够了 &#x2F;proc 的混乱。他们决定<strong>专门为设备、总线、驱动</strong>设计一个<strong>新的文件系统</strong>，这就是 <code>sysfs</code>。</p></li><li><p><strong>核心规则</strong>：严格的层次结构和<strong>一个文件只干一件事（只包含一个值</strong>的原则。比如，你想看设备的电源状态，在 sysfs 里就是一个单独的文件，里面只有 0 或 1，不需要任何解析。</p></li><li><p><strong>本质</strong>：<code>sysfs</code> 就是内核中 <code>kobject</code>（内核对象的基类）树状拓扑结构在<strong>用户空间的直接映射</strong>。</p></li></ul><p><strong>&#x2F;sys 的核心目录</strong>（<code>LDM(linux设备模型</code>) 的三维立体图）</p><ul><li><code>/sys/devices/</code>：<ul><li><strong>族谱视角</strong>, <strong>全局设备树最真实的物理连接图</strong>。这是系统所有物理设备的真实拓扑结构（谁挂在谁下面，一清二楚）。</li></ul></li><li><code>/sys/bus/</code>：<ul><li><strong>相亲视角, 总线match视角</strong>。包含 platform、i2c、usb 等。目录下的 devices 和 drivers 实际上是指向 &#x2F;sys&#x2F;devices&#x2F; 的符号链接（这也是为什么你能在platform 下找到你创建的设备）。</li><li>在 <code>/sys/bus/platform/</code> 下，你可以清楚地查账哪些设备挂在 platform 总线上？哪些驱动已经就绪？谁和谁配对成功了</li></ul></li><li><code>/sys/class/</code>：<ul><li><strong>职业视角</strong>。不管你是通过什么总线接入的，只要你是 <strong>LED</strong>，就归类到 <code>/sys/class/leds/</code>；是<strong>网卡</strong>，就归类到 <code>/sys/class/net/</code>。</li></ul></li><li><code>/sys/module/</code>：系统加载的所有内核模块（ko文件）的状态。</li></ul><hr><h2 id="我如何在驱动中创建-sys下的一些东西"><a href="#我如何在驱动中创建-sys下的一些东西" class="headerlink" title="我如何在驱动中创建&#x2F;sys下的一些东西"></a>我如何在驱动中创建&#x2F;sys下的一些东西</h2><p>在 sysfs 中，有一个极其优雅的设计哲学：</p><p><strong>目录代表“对象” (kobject)</strong>，而<strong>文件代表对象的“属性” (attribute)</strong>。</p><ul><li>在你的<strong>驱动里</strong>，你只需要<strong>提供两个函数（读和写）</strong>，内核就会自动帮你把它们<strong>映射成 &#x2F;sys 下的一个普通文本文件</strong>。</li><li>用户层只需使用最基础的 <code>cat</code> 和 <code>echo</code> 命令，就能跨越内核边界，直接调用你写的 C 函数！<blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-08-0309230347.png" alt="alt text"></p></blockquote></li></ul><h2 id="sysfs源码目录"><a href="#sysfs源码目录" class="headerlink" title="sysfs源码目录"></a>sysfs源码目录</h2><p>sysfs这些属于VFS虚拟文件系统，所以</p><ul><li><strong>Sysfs 核心机制</strong>：<code>fs/sysfs/</code><ul><li>sysfs 文件系统的挂载、文件读写回调的底层实现。</li></ul></li><li><strong>Kobject</strong> (sysfs的灵魂)<code>lib/kobject.c</code><ul><li>重点！kobject 负责维护引用计数，它的每一次注册（kobject_add）都会在 sysfs 里生成一个目录。</li></ul></li><li><strong>与设备模型的桥梁</strong>:<code>drivers/base/core.c</code> <code>drivers/base/class.c</code><ul><li>这里包含了 <code>class_create</code> 等函数，它们<strong>内部</strong>会调用 <code>kobject_add</code> 和 <code>sysfs 的 API</code>，最终在 &#x2F;sys&#x2F; 下建出你看到的结构。</li></ul></li></ul><h2 id="疑问：class-device是为了在sysfs里面创建节点，那么申请设备号，注册设备的作用是什么？"><a href="#疑问：class-device是为了在sysfs里面创建节点，那么申请设备号，注册设备的作用是什么？" class="headerlink" title="疑问：class,device是为了在sysfs里面创建节点，那么申请设备号，注册设备的作用是什么？"></a>疑问：class,device是为了在sysfs里面创建节点，那么申请设备号，注册设备的作用是什么？</h2><p>在linux的内核设备-驱动-总线模型中</p><ul><li>我们用<strong>dts来描述设备信息</strong>（属于<strong>总线层</strong>），<ul><li>最终得到<code>platform_device</code></li><li><strong>作用</strong>，是<strong>描述硬件资源</strong>（寄存器地址、中断号），此时，<strong>内核已经知道硬件的存在了</strong></li><li><blockquote><p><strong>但用户空间的程序（如你的 C 语言 App）还看不见它</strong>，<strong>没有接口</strong></p></blockquote></li></ul></li><li><strong>注册字符设备</strong>&#x2F;<strong>申请设备号</strong>（属于<strong>文件系统层</strong>）<ul><li><strong>目的</strong>：在 <code>/dev/</code> 目录下生成一个“<strong>文件节点</strong>”</li><li><blockquote><p>Linux 的哲学是“万物皆文件”。用户 App 想要控制硬件，必须通过 open(“&#x2F;dev&#x2F;myled”, …)</p></blockquote></li><li><strong>申请设备号</strong>, <strong>注册 cdev</strong>，就是为了<strong>告诉内核</strong>：“当有人<code>访问这个 /dev/</code> 文件时，请<strong>跳转到我驱动</strong>里写的 <code>read/write/ioctl</code> 函数去</li></ul></li></ul><h3 id="现代驱动开发的不同"><a href="#现代驱动开发的不同" class="headerlink" title="现代驱动开发的不同"></a>现代驱动开发的不同</h3><p>上面你还要自己手动让内核把用户层的访问，路由到驱动</p><p>在<strong>现代驱动开发</strong>中，我们确实在<strong>减少直接操作设备号</strong></p><ul><li><strong>子系统封装（如 LED 子系统）</strong>：<ul><li>如果你写的是<strong>标准的 <code>leds-gpio</code> 驱动</strong>，你不需要申请设备号，也不需要 cdev_add</li><li>因为 <strong>LED 子系统框架</strong>（<code>led-class.c</code>）已经<strong>提前申请好了主设备号</strong>，并统一管理了 <code>/sys/class/leds/</code>。你只需要调<code>led_classdev_register</code>，框架就会自动帮你搞定一切</li></ul></li><li><blockquote><p>对比</p></blockquote></li><li><blockquote><p><img src="/images/platform%E8%AE%BE%E5%A4%87%EF%BC%8C%E9%A9%B1%E5%8A%A8%E6%A2%B3%E7%90%86-09-0309230347.png" alt="alt text"></p></blockquote></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#platform-device-driver%E6%A2%B3%E7%90%86&quot;&gt;platform device driver梳理&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E6%9E%84%E4%BD%93%E6%8</summary>
      
    
    
    
    <category term="学习笔记" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="LINUX" scheme="https://liangji-seu.github.io/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%B5%8C%E5%85%A5%E5%BC%8F/LINUX/"/>
    
    
    <category term="嵌入式" scheme="https://liangji-seu.github.io/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F/"/>
    
    <category term="linux" scheme="https://liangji-seu.github.io/tags/linux/"/>
    
    <category term="驱动" scheme="https://liangji-seu.github.io/tags/%E9%A9%B1%E5%8A%A8/"/>
    
  </entry>
  
</feed>
