<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>钟鼓楼</title>
  
  <subtitle>钟楼瘦，鼓楼胖</subtitle>
  <link href="https://thysrael.github.io/atom.xml" rel="self"/>
  
  <link href="https://thysrael.github.io/"/>
  <updated>2026-04-10T13:34:12.900Z</updated>
  <id>https://thysrael.github.io/</id>
  
  <author>
    <name>Thysrael</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>信息安全-TEE</title>
    <link href="https://thysrael.github.io/posts/9323e869/"/>
    <id>https://thysrael.github.io/posts/9323e869/</id>
    <published>2026-04-09T09:58:45.000Z</published>
    <updated>2026-04-10T13:34:12.900Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>TEE（Trusted Excution Enviroment）我一直没有学明白过，也是借着 recycle 学长的论文的机会，重新学习了一下。</p><p>我觉得我一直没有学明白的原因是，TEE 涉及的技术太繁多了（内存加密，安全世界，加密计数器，可信报告，……），而又缺少核心定义，以至于我对于到底啥是 TEE 一直没有具体的概念，反而被上面描述的技术或者概念牵着鼻子走。而实际上这些技术与概念，往往是为了应对某种具体的攻击场景，并不本质。以内存加密为例，难道它只能用在 TEE 中吗？显然不是的，这套加密算法可以被用于任何容易遭受物理攻击或者重放（replay）攻击的地方；同样的，也不是说 TEE 就一定需要内存加密，如果没有物理攻击（比如说手机，或者 HBM 这种天生对物理攻击防御能力强的硬件），也可以不需要内存加密。</p><hr><h2 id="二、硬件信任根"><a href="#二、硬件信任根" class="headerlink" title="二、硬件信任根"></a>二、硬件信任根</h2><p>那么 TEE 的核心到底是什么？我觉得是它把信任根（Root of Trust）明确放在了<strong>硬件</strong>上（粗浅说应该就是 CPU 上），而且<strong>只有硬件</strong>上。这是什么意思呢？就是我们其实之前的信任根，往往是某个软件。比如说我们在跑一个进程的时候，不担心另一个进程看到这个进程内部的数据，这是因为我们相信 OS 会正确配置我们的页表，确保两个进程的虚拟地址空间的隔离性。</p><p>在这个过程中，其实同样也是有硬件参与的，比如说 MMU 进行地址翻译，也是硬件过程。但是我们依然说，我们信任的是 OS 而非硬件。这是因为比较常见的情境下，硬件一般就是“乙方”，而软件才是“甲方”，软件负责按照一定规则使用硬件，硬件基本上也不能抗拒。而这种方式下，如果软件是恶意的，就算硬件有心反抗，也是没有办法的事情。所以我们才说，一般情形下，我们其实信任的是一个高特权级的软件。</p><p>那么为什么我们无法再信任软件了呢？我觉得其实有两方面原因，一方面比较传统，那就是高特权级的软件可能被黑客攻击了，它自然可以窃取低特权级的信息了，比如说黑客拿到了 Root 权限，查看某个进程的虚拟地址空间的机密数据，这就非常容易了。但是我觉得这种场景，其实不太能够完全催生 TEE 的诞生，因为最直白的方法，是通过修复 bug 来阻止黑客拿到 Root 权限，而不是构建一个连 OS 都控制不了的 TEE，通过牺牲便捷性，来保证安全。另一个方面则是云服务时代的到来，用户开始向云厂商租赁服务器等资源，此时的 OS 或者 VM 这种高特权级软件，往往是云厂商提供的。都不需要黑客，只需要云厂商有一点恶意的念头，就可以把租户的数据窃取出来。这种不信任所涵盖的范围（OS，VM，Hypervisor）是之前的单点攻击所不具备的（Root 的危害性跟它们比起来弱爆了）。这个时候租户迫切需要一种完全不涉及任何高特权级软件的信任根，也就是由硬件去保证的信任根。当然了，第二点在智能手机来临以后，又变成了手机的生产厂商，对于手机用户的不信任了，手机用户可以获得手机的 Root 权限（故意的，或者无意的），进而将手机厂商或者用户的敏感数据泄露。</p><p>那是不是意味着我们只能信任硬件呢，顶多再加上上面的固件呢？并不是，因为信任根所能提供的功能实在是太有限了（无法管理设备，无法提供充足的编程抽象，……）。所以我们其实会使用一些软件的。但是我们该如何保证这些软件的可信性呢？我们使用信任链来保证，也就是如果我们信任这个硬件，而硬件信任软件，则我们信任软件。通过这种方式，我们从信任根（也就是信任链的头部）开始，构建出一整个信任基（Trusted Compute Base，TCB）。</p><p>当然这个时候就有一个疑问了，那就是如果我们可以使用信任链，那为什么我们不能把云厂商的 VM 也加入信任链中，这样不就可以避免云厂商的恶意了吗？我觉得有两个方面的原因：一方面是信任的成本是非常高的，我们为了验证软件是可信任的，通常需要用各种方式检验它是否被篡改，即使未被篡改，我们也需要保证它之前也是不含恶意或者 bug 的（这点就非常难了）。另一方面是，信任链越长，可信基越大，那么攻击面也就越广，反而不利于保证安全。</p><p>但是 TCB 如果较小，那么往往就对应着 TEE 较为受限的功能。那么 TEE 如何与普通计算环境的协作，如何在功能性和安全性上做 tradeoff，就成了 TEE 的一个重要的设计课题。</p><p>最后总结一下，为了建立信任，我们需要一个被信任的“上帝”，也就是信任根。而传统扮演信任根的高特权级软件，失去了我们的信任，因此我们选择信任硬件作为信任根。</p><hr><h2 id="三、组件"><a href="#三、组件" class="headerlink" title="三、组件"></a>三、组件</h2><h3 id="3-1-总论"><a href="#3-1-总论" class="headerlink" title="3.1 总论"></a>3.1 总论</h3><p>依然先强调，这里出现的相关的组件，并不是一定会出现在每一个 TEE 机制中的。有些 TEE 是缺少部分组件的。</p><h3 id="3-2-硬件隔离"><a href="#3-2-硬件隔离" class="headerlink" title="3.2 硬件隔离"></a>3.2 硬件隔离</h3><p>这说的是，CPU 会提前准备一片物理内存区域，这片内存区域不会被 OS 看到，只有在 CPU 进入到特定模式的时候才可以看见。而 CPU 是否进入这个特定模式，也不由 OS 控制。</p><p>总的来说，这其实是相当于构建了一层更高级的特权级，这个特权级比 OS 或者 Hypervisor 的权限还要高。它的工程实现，就是某个寄存器上有个 bit 来指定它是否存在于这个特定模式（在 ARM 中就是是否在 EL3），然后 CPU 在访问这个隔离内存的时候，都会检查一下这个 bit，如果没有置位，就会触发异常。</p><p>硬件隔离机制确实是所有 TEE 必备的要素。因为隔离就是安全的最基础的机制。</p><h3 id="3-3-远程校验"><a href="#3-3-远程校验" class="headerlink" title="3.3 远程校验"></a>3.3 远程校验</h3><p>一般的 TEE 是信任硬件的，也就是信任硬件和其上的 TCB 是未被篡改的。但是能不能更进一步的，就是并不是无条件信任硬件，而是我们真的可以校验一下我们使用的硬件，这个硬件是否是可信的，这就是远程校验（remote attestation）的思路。</p><p>具体而言，就是硬件会在启动后生成一份安全报告，来表明目前自己的状况（应该是哈希吧）。我们只需要拿着这份报告去与官方的报告去比对是否相同，就可以确认 TCB 是否被篡改。</p><h3 id="3-4-加密内存"><a href="#3-4-加密内存" class="headerlink" title="3.4 加密内存"></a>3.4 加密内存</h3><p>如果只有硬件隔离机制，那么那片隔离内存中的数据，其实是明文存储的。但是这也没有关系，进程隔离的时候，虚拟地址空间的数据也是明文存储的，也没有见到有什么需要防范的必要。</p><p>但是如果我们考虑物理攻击手段，那就不一定了。也就是攻击者直接把内存拆下来，然后读取其中的内容。这个时候仅依靠硬件隔离就不行了。</p><p>所以我们可以用一个密钥把内存加密了，然后把密钥放在 CPU 上，进出 CPU 的都是密文，解密和重加密过程都发生在 CPU 的内部。</p><h3 id="3-5-完整性保护"><a href="#3-5-完整性保护" class="headerlink" title="3.5 完整性保护"></a>3.5 完整性保护</h3><p>即使完全看不见机密的数据，攻击者依然可以展开一些攻击，比如说：</p><ul><li>重放攻击</li><li>重排攻击</li><li>篡改攻击</li></ul><p>为了应对这些攻击，我们引入了完整性保护技术。这个技术由两个部分组成，MAC（Message Authentication Code）和 Merkle Tree。</p><p>MAC 主要用于防御这上面的三种攻击。对于一个数据 <code>data</code> ，我们有：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">MAC <span class="token operator">=</span> func<span class="token punctuation">(</span>data<span class="token punctuation">,</span> key<span class="token punctuation">,</span> address<span class="token punctuation">,</span> counter<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>简单来说，<code>key</code> 是避免伪造数据的密钥，<code>address</code> 是数据的地址，<code>counter</code> 是数据的版本号。当我们把这些数据都通过某个特定的函数生成一个 summary，也就是 MAC 后，我们只需要比较我们计算出来的 MAC，和内存中原本保存的 MAC 是否相同，就可以确定 data 是否是特定的 address 特定的版本的数据了。</p><p>此时篡改和重排彻底失效，但是重放没有完全失效。MAC 对于重放的防御，依赖于 counter：攻击者如果只重放了 data 的内容（相当于回退版本了），但是没有回退 counter，那么就可以被 MAC 检测出来。但是如果攻击者同时也回退了 counter 呢？那么 MAC 就无能为力了。</p><p>然后我们来说一下 MAC 的粒度。粒度可以是整个隔离内存吗？并不可以，这太大了，并不方便。我们需要将其分成多个 chunk 分别进行 MAC。那么 chunk 的大小应该是多少呢？答案是一个 cache line 的大小（L1 / L2 / L3 cache line 大小通常一致，访存粒度也是相同），这跟 cache 本身没有关系，而是说这个大小，是 CPU 访问 DRAM 的大小。相当于是 CPU 每次访问内存的时候，都会做一次 MAC 的校验。</p><p>那么我们该如何完整防御重放攻击呢？就是使用 Merkle Tree。我理解的是，相当于是 Merkle Tree 提供了一种控制整个内存在同一个版本的能力。至于为什么使用 Tree 结构，是因为直接 trival 的控制，成本太高了。根的版本号保存在 CPU 中，避免被篡改。</p><hr><h2 id="四、案例"><a href="#四、案例" class="headerlink" title="四、案例"></a>四、案例</h2><h3 id="4-1-ARM-TrustZone"><a href="#4-1-ARM-TrustZone" class="headerlink" title="4.1 ARM TrustZone"></a>4.1 ARM TrustZone</h3><p>这个案例中，就是只有硬件隔离的 TEE，并没有内存加密等组件。相当于引入了 EL3, 通过设置 <code>NS</code> bit，来区分 Normal World 和 Secure World。在 Normal World 里跑 Normal OS，在 Secure World 里跑 Secure OS。总的来说 TCB 还是挺大的。</p><p>之所以只有一个硬件隔离机制，是因为 TrustZone 并不是专门为了云服务器开发的，而是为了端侧手机开发的。所以并不会有一个黑客采用物理攻击手段。主要用于防止软件攻击。</p><h3 id="4-2-Intel-SGX-AMD-SEV"><a href="#4-2-Intel-SGX-AMD-SEV" class="headerlink" title="4.2 Intel SGX / AMD SEV"></a>4.2 Intel SGX / AMD SEV</h3><p>更小的可信基，只有一段代码，被成为 enclave，OS / hypervisor 都不可信。首次引入了远程验证的机制。在使用的时候，普通程序代码通过 <code>icall</code> 来调用 enclave 的代码，就像调用函数一样。enclave 的代码，用 <code>ecall</code> 调用普通函数。</p><p>虽然这样可信基确实小了，但是相当于要改写程序，专门为 enclave 写代码，所以并不是很方便。而且 enclave 可以使用的隔离内存的大小是有限的。</p><p>这种最细粒度的机制，同样不是为了云厂商开发的，而是为了保护敏感数据，进行机密计算，比如说进行视频解密，防盗版等。</p><h3 id="4-3-Intel-TDX-AMD-SEV-SNP"><a href="#4-3-Intel-TDX-AMD-SEV-SNP" class="headerlink" title="4.3 Intel TDX / AMD SEV-SNP"></a>4.3 Intel TDX / AMD SEV-SNP</h3><p>将整个 VM 作为 TEE，也就是说，恶意的 Hypervisor 是看不到 VM 里的内容的，Hypervisor 只能调度，而不能修改。这种技术基本上就是为了云服务开发的。</p><p>相当于租户可以完全透明的使用 VM，不用担心云厂商的窃取，也不用引入额外的修改。</p><h3 id="4-4-ARM-CCA"><a href="#4-4-ARM-CCA" class="headerlink" title="4.4 ARM CCA"></a>4.4 ARM CCA</h3><p>也是为云场景开发的。我理解就是有多个 Realm，但是我也不知道 Realm 是什么，大概就是 VM 吧。不过它有一个自己运行在 EL2 的 Hypervisor，被叫作 RMM（Realm Management Monitor）。</p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;TEE（Trusted Excution Enviroment）我一直没有学明白过，也是借着 recycle 学长的论文的机会，重新学习了一下。&lt;/p&gt;
&lt;p&gt;我觉得我一直没有学明白的原因是，TEE 涉及的技术太繁多了（内存加密，安全世界，加密计数器，可信报告，……），而又缺少核心定义，以至于我对于到底啥是 TEE 一直没有具体的概念，反而被上面描述的技术或者概念牵着鼻子走。而实际上这些技术与概念，往往是为了应对某种具体的攻击场景，并不本质。以内存加密为例，难道它只能用在 TEE 中吗？显然不是的，这套加密算法可以被用于任何容易遭受物理攻击或者重放（replay）攻击的地方；同样的，也不是说 TEE 就一定需要内存加密，如果没有物理攻击（比如说手机，或者 HBM 这种天生对物理攻击防御能力强的硬件），也可以不需要内存加密。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;二、硬件信任根&quot;&gt;&lt;a href=&quot;#二、硬件信任根&quot; class=&quot;headerlink&quot; title=&quot;二、硬件信任根&quot;&gt;&lt;/a&gt;二、硬件信任根&lt;/h2&gt;</summary>
    
    
    
    <category term="信息安全" scheme="https://thysrael.github.io/categories/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="信息安全" scheme="https://thysrael.github.io/tags/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/"/>
    
    <category term="S12课上" scheme="https://thysrael.github.io/tags/S12%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
  <entry>
    <title>办公工具-依赖地狱</title>
    <link href="https://thysrael.github.io/posts/c55757b3/"/>
    <id>https://thysrael.github.io/posts/c55757b3/</id>
    <published>2026-02-06T08:14:46.000Z</published>
    <updated>2026-02-11T07:07:52.229Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、菱形依赖"><a href="#一、菱形依赖" class="headerlink" title="一、菱形依赖"></a>一、菱形依赖</h2><p>所有的依赖问题，大抵都会与“菱形依赖”有关。也就是如图所示， <code>Web Lib</code> 和 <code>Log Lib</code> 都依赖于 <code>JSON Lib</code>（相当于菱形的下半部分）：</p><p><img src="/posts/c55757b3/diamond-dependency.png" alt="菱形依赖项"></p><p><code>Web Lib</code> 和 <code>Log Lib</code> 对于 <code>JSON Lib</code> 的依赖要求是不同的，<code>Web Lib</code> 要求 <code>&gt;=1.0</code> ，而 <code>Log Lib</code>要求 <code>&gt;= 2.0</code> ，最终的 <code>Log Lib</code> 需要同时满足这两种条件，也就是 <code>&gt;= 2.0</code> 。</p><p>而当这写条件无法满足的时候，这个系统就崩溃了。不过如果软件都保持向后兼容性（backward compability），那么按理说应该我们总能通过“装最新版”的方式来解决依赖问题。但问题就在于，这里是现实：</p><ul><li>并不是所有的软件都保证了向后兼容性</li><li>并不总能安装特定版本的依赖（硬件不支持，平台已经下架）</li><li>……</li></ul><hr><h2 id="二、多版本共存"><a href="#二、多版本共存" class="headerlink" title="二、多版本共存"></a>二、多版本共存</h2><h3 id="2-1-总论"><a href="#2-1-总论" class="headerlink" title="2.1 总论"></a>2.1 总论</h3><p>当我们需要不同版本的依赖的时候，有一个办法通用的办法，就是存储多个版本的依赖，这些依赖分别满足难以兼容的需求们。</p><p>但是并不是只要有多个版本的依赖共存，就可以完全解决这个问题了。并没有那么简单。因为此时的软件系统中，就会存在多个版本的依赖，运行和管理都会成为问题。</p><p>不同的语言，多版本依赖的支持也是不同的，下面我们会分别展开介绍：</p><h3 id="2-2-全局唯一命名"><a href="#2-2-全局唯一命名" class="headerlink" title="2.2 全局唯一命名"></a>2.2 全局唯一命名</h3><p>代表的语言就是 C 和 Python。</p><p>也就是说，他们是不支持在一个系统中，同时存在两个不同版本的相同依赖的。也就是说，他们一旦遇到菱形依赖冲突的情况，就彻底无法解决了。</p><p>C 我不知道，但是 python 的包管理器 <code>pip</code>，在安装某个包的时候，如果系统中存在不满足的依赖的时候，它会直接把依赖卸载掉，装上符合自己要求版本的依赖。这就会导致，可能就安装一个包，就会导致其他包用不了了。</p><h3 id="2-3-自己携带自己的依赖"><a href="#2-3-自己携带自己的依赖" class="headerlink" title="2.3 自己携带自己的依赖"></a>2.3 自己携带自己的依赖</h3><p>代表的包管理器是 <code>npm</code>。</p><p>也就是说，当一个包需要某个依赖的时候，它不会安装在全局，或者安装在项目中，而是会安装在自己的 <code>node_modules</code> 下，在运行时，只会依赖自己的库。也就是说，即使两个包依赖相同版本的库，也会在系统中存在两份一模一样的库。</p><h3 id="2-4-全局依赖"><a href="#2-4-全局依赖" class="headerlink" title="2.4 全局依赖"></a>2.4 全局依赖</h3><p>代表的语言是 Rust 和 Go。</p><p>Rust 允许同名不同版本的 crate 链接进同一个 binary。Go 强制要求主版本号升级（v1 到 v2）时，必须修改包的导入路径（例如 <code>github.com/pkg/d</code> 变成 <code>github.com/pkg/d/v2</code>），相当于不同版本的包，就是不同的包。</p><hr><h2 id="三、Linux-发行版更新"><a href="#三、Linux-发行版更新" class="headerlink" title="三、Linux 发行版更新"></a>三、Linux 发行版更新</h2><h3 id="3-1-总论"><a href="#3-1-总论" class="headerlink" title="3.1 总论"></a>3.1 总论</h3><p>整个 GNU/Linux 系统，也可以看作是需要保证依赖没有问题的复杂软件系统。因此也有不同的依赖管理方式。</p><h3 id="3-2-固定版本"><a href="#3-2-固定版本" class="headerlink" title="3.2 固定版本"></a>3.2 固定版本</h3><p>固定版本（Fixed Release），代表为 Ubuntu。</p><p>具有不同的版本，比如说 Ubuntu 22.04 。在特定版本中，所有的软件的版本都是写死的，也就是说，在安装了以后，在 <code>apt upgrade</code> 基本上是没有意义的，并没有办法将某个软件，比如说 emacs 进行升级。</p><p>当然这也是有些过于绝对的，基本上我们在一个特定的 Ubuntu 版本中更新，主要是为了更新安全补丁。</p><h3 id="3-3-滚动更新"><a href="#3-3-滚动更新" class="headerlink" title="3.3 滚动更新"></a>3.3 滚动更新</h3><p>滚动更新（Rolling Release），代表为 Archlinux。</p><p>不再有固定的版本，所有的软件都维持最新的。我觉得也挺有意思的，相当于默认假设每个依赖都具有向后兼容性，这才使得将所有依赖都变成最新的，成为可能。</p><p>当然事实不会是这样，我们总会因为一些软件，失去向后兼容性，或者恰好不能保持系统完全的最新。导致 ArchLinux 会出现滚挂的局面。</p><h3 id="3-4-快照更新"><a href="#3-4-快照更新" class="headerlink" title="3.4 快照更新"></a>3.4 快照更新</h3><p>快照更新（Snapshot Release），代表为 NixOS。</p><p>我觉得更类似于 project 安装依赖的感觉，类似于 lockfile 的感觉，强调原子更新与可复现性。</p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、菱形依赖&quot;&gt;&lt;a href=&quot;#一、菱形依赖&quot; class=&quot;headerlink&quot; title=&quot;一、菱形依赖&quot;&gt;&lt;/a&gt;一、菱形依赖&lt;/h2&gt;&lt;p&gt;所有的依赖问题，大抵都会与“菱形依赖”有关。也就是如图所示， &lt;code&gt;Web Lib&lt;/code&gt; 和 &lt;code&gt;Log Lib&lt;/code&gt; 都依赖于 &lt;code&gt;JSON Lib&lt;/code&gt;（相当于菱形的下半部分）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/c55757b3/diamond-dependency.png&quot; alt=&quot;菱形依赖项&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Web Lib&lt;/code&gt; 和 &lt;code&gt;Log Lib&lt;/code&gt; 对于 &lt;code&gt;JSON Lib&lt;/code&gt; 的依赖要求是不同的，&lt;code&gt;Web Lib&lt;/code&gt; 要求 &lt;code&gt;&amp;gt;=1.0&lt;/code&gt; ，而 &lt;code&gt;Log Lib&lt;/code&gt;要求 &lt;code&gt;&amp;gt;= 2.0&lt;/code&gt; ，最终的 &lt;code&gt;Log Lib&lt;/code&gt; 需要同时满足这两种条件，也就是 &lt;code&gt;&amp;gt;= 2.0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;而当这写条件无法满足的时候，这个系统就崩溃了。不过如果软件都保持向后兼容性（backward compability），那么按理说应该我们总能通过“装最新版”的方式来解决依赖问题。但问题就在于，这里是现实：&lt;/p&gt;</summary>
    
    
    
    <category term="办公工具" scheme="https://thysrael.github.io/categories/%E5%8A%9E%E5%85%AC%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="S11课上" scheme="https://thysrael.github.io/tags/S11%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="办公工具" scheme="https://thysrael.github.io/tags/%E5%8A%9E%E5%85%AC%E5%B7%A5%E5%85%B7/"/>
    
    <category term="工具总结" scheme="https://thysrael.github.io/tags/%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-流浪苏州</title>
    <link href="https://thysrael.github.io/posts/29a993d6/"/>
    <id>https://thysrael.github.io/posts/29a993d6/</id>
    <published>2026-02-02T10:47:18.000Z</published>
    <updated>2026-02-02T17:20:26.818Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、友情"><a href="#一、友情" class="headerlink" title="一、友情"></a>一、友情</h2><blockquote><p>莫愁前路无知己，天下谁人不识君。</p></blockquote><p>我很少谈起朋友，真的很少谈起。或许在我的潜意识里，我一直都不是一个好朋友。我的意思是，我总是不断的挣扎，直到把所有人远远的推开。但是在偶然的相遇，和必然的分离之间，依然有一些值得称道的情谊。</p><p><img src="/posts/29a993d6/c7f453d58fe2b6f78e2386a9f4f7595b.jpg" alt="合照"></p><p>酸了这么多，大概只是为了说一下，这次去苏州是和马博一起去的，然后体验非常不错。跟着马博去最大的好处就是，不用动脑子想去哪里，马博都规划好了。当然最大的缺点就是，我现在完全想不起来苏州我们玩过哪里了，真的是一点也记不住了。</p><hr><h2 id="二、观感"><a href="#二、观感" class="headerlink" title="二、观感"></a>二、观感</h2><p>苏州是我见过最友善的城市，哇，简直跟扬州形成了非常鲜明的对比。</p><p>我们一路上见到的人，每一个人都非常喜气洋洋的，而且都非常热情，善于聊天。我和马博两个 I 人，从未主动找人攀谈，但是基本上每顿饭服务员都会与我们聊上两句，问问我们有没有去看什么演出，并给我们推荐一些要去的地方，或者直接非常大方地教我们怎么点菜才能更便宜。</p><p>而且他们的热情不带丝毫的虚伪或者造作，就感觉是完全发自肺腑的。可能是太习惯虚伪的社交礼仪了，我甚至都想戳破这种如梦似幻的真诚了。但是非常遗憾，我完全找不到任何破绽。</p><p>我遇到很多上海人，他们都提到他们会在苏州买房子，我觉得除了经济原因之外，可能这里如此热情真诚的民风有关吧。</p><p>除了民风的问题之外，我觉得这边和扬州类似，都是属于古建筑和现代风格交织，自然而且纯粹。而交通方面，虽然有地铁，但是还是有一些地铁覆盖不到的路程，而苏州的共享单车非常少（比较有特点的是，它们会上车牌），导致步行的时间有点长了。</p><p><img src="/posts/29a993d6/c51d16653b5dbe7383d07797f7a868a4.jpg" alt="共享单车车牌"></p><p>最后，民宿房东姐姐确实<strong>人美心善</strong>！</p><hr><h2 id="三、景点"><a href="#三、景点" class="headerlink" title="三、景点"></a>三、景点</h2><p>元旦假期我们没有订票，所以很多知名的景点都去不成了。但是众所周知，马博会找到真正的景点，或者说，马博现在去过的景点，在未来都会变成知名景点。马博是真正的时间刺客！</p><h3 id="3-1观前街"><a href="#3-1观前街" class="headerlink" title="3.1观前街"></a>3.1观前街</h3><p>到了那里之后，我们就直接去往观前街去逛了。观前街作为商业街，我觉得其实没有什么出奇的，不过这个壁画我确实很喜欢：</p><p><img src="/posts/29a993d6/e667cd1e51ae813c6cc161d3977fd0b4.jpg" alt="平江路"></p><p>比较有特点是，一直有条河穿越其中，所以会显得非常水乡气质：</p><p><img src="/posts/29a993d6/82b17f79508cc3cc4ea4bf9b4d37e9eb.jpg" alt="水乡"></p><h3 id="3-2-报恩寺"><a href="#3-2-报恩寺" class="headerlink" title="3.2 报恩寺"></a>3.2 报恩寺</h3><p>在逛完观前街之后，就已经很晚了，但是我和马博游兴未减，所以又沿着平江路走，逛到了报恩寺：</p><p><img src="/posts/29a993d6/a0277fb86a06f84de4e7b99247f1f371.jpg" alt="报恩寺"></p><p>我们也没有进去看（早就关门了），不过报恩寺里的塔是真的很壮观，这可能跟周围没有其他高耸的建筑物有关，有一种独一无二的震撼感：</p><p><img src="/posts/29a993d6/7f3185a118f0bd72754d2ba9e95ce3bf.jpg" alt="塔"></p><h3 id="3-3-寒山寺-枫桥"><a href="#3-3-寒山寺-枫桥" class="headerlink" title="3.3 寒山寺 - 枫桥"></a>3.3 寒山寺 - 枫桥</h3><p>我们第二天一早晨起来就去寒山寺了，但是看到 20 一位的票价，我们还是犹豫了，最终选择在寺院外面看猫猫：</p><p><img src="/posts/29a993d6/9ce54b4e9f49bf9dd5b1d2d09946f100.jpg" alt="寒山寺猫猫"></p><p>枫桥就在寒山寺的后面，我觉得风景很好很有古诗的意境，只可惜假期人实在是太多了：</p><p><img src="/posts/29a993d6/c61f521384ee31bcdaef81f97c0dac91.jpg" alt="枫桥"></p><p>穷鬼只能遥遥眺望寒山寺：</p><p><img src="/posts/29a993d6/a3aabe6f5f04a6f2b26a9919dfc8b4b7.jpg" alt="寒山寺"></p><h3 id="3-4-西园寺"><a href="#3-4-西园寺" class="headerlink" title="3.4 西园寺"></a>3.4 西园寺</h3><p>我们从寒山寺离开后，有往西园寺奔，只可惜去得太晚了，素斋的队排得很长：</p><p><img src="/posts/29a993d6/544086ddfef8f9d635a2666ca7bf77af.jpg" alt="西园寺"></p><p>我们只好逛了逛就出来了。我觉得西园寺作为寺庙景点来说，不如扬州的寺庙有趣庄严。</p><h3 id="3-5-东方之门"><a href="#3-5-东方之门" class="headerlink" title="3.5 东方之门"></a>3.5 东方之门</h3><p>我一直以为东方之门这种地标性建筑物，会建在苏州的市中心呢，没有想到它建在苏州的郊区，不过一出地铁口就感觉到非常的震撼：</p><p><img src="/posts/29a993d6/635ee72026af1e3ee53b451d096acc73.jpg" alt="东方之门1"></p><p>当然晚上看也是别有风味：</p><p><img src="/posts/29a993d6/e24a52a6b1dce21bc4ff9202ad47a140.jpg" alt="东方之门2"></p><p>其实我很好奇东方之门上面有什么，而实际上经过 web search，发现原来东方之门是一栋烂尾楼，高层完全没有被开发。我们按照小红书的介绍去到的最高楼层是 138 层，只可惜原本说的消防通道关闭了，而 138 层的唯一一家商户（理发店），不让我们照相。最后只在 35 层照一张相：</p><p><img src="/posts/29a993d6/e6c4adeba04d13def46346f067ba4ebb.jpg" alt="35 层"></p><p>东方之门是我少见的看见苏州衰败一面的建筑。苏州给我的感觉一直是完美无暇的。</p><h3 id="3-6-金鸡湖"><a href="#3-6-金鸡湖" class="headerlink" title="3.6 金鸡湖"></a>3.6 金鸡湖</h3><p>在东方之门下面，就是金鸡湖，金鸡湖马博一直想坐水上公交，只可惜那天水上公交关停了。</p><p>小红书说，在日落的时候，从金鸡湖畔的大相框看过去，可以看到落日穿过东方之门。但是并没有，三点并不共线，所以“豆包，启动！”：</p><p><img src="/posts/29a993d6/40ed05a20ec9a6ee827e321d48997272.jpg" alt="三点一线"></p><p>夜景的美丽，甚至超过了晚霞，让我和马博感慨，不应该在室内休息这么久的：</p><p><img src="/posts/29a993d6/33ce56429714389791bcd86036aae84e.jpg" alt="金鸡湖夜景"></p><h3 id="3-7-音乐喷泉"><a href="#3-7-音乐喷泉" class="headerlink" title="3.7 音乐喷泉"></a>3.7 音乐喷泉</h3><p>再次赞美马博，他居然知道金鸡湖只在节日举办的音乐喷泉。</p><p>音乐喷泉真的好好看呀：</p><p><img src="/posts/29a993d6/e21d5d13404abd61926b83d97d0a623c.jpg" alt="e21d5d13404abd61926b83d97d0a623c"></p><p><img src="/posts/29a993d6/a64eca12d5e6759e9761a3d522614054.jpg" alt="a64eca12d5e6759e9761a3d522614054"></p><p><img src="/posts/29a993d6/eb758c466c0ece7101c59c584c2a3995.jpg" alt="eb758c466c0ece7101c59c584c2a3995"></p><p><img src="/posts/29a993d6/b31518701409e4921cc6b6fa0ad629fa.jpg" alt="b31518701409e4921cc6b6fa0ad629fa"></p><p><img src="/posts/29a993d6/3a964bb2153281bc29dc80e2b0ec2879.jpg" alt="3a964bb2153281bc29dc80e2b0ec2879"></p><h3 id="3-8-火车站"><a href="#3-8-火车站" class="headerlink" title="3.8 火车站"></a>3.8 火车站</h3><p>谁家火车站临江而建啊，实在是太漂亮了：</p><p><img src="/posts/29a993d6/1b629c8e94dc45c1e83b91eccc3e11f4.jpg" alt="火车站"></p><p>这个江上的行船是真的可以乘坐啊，15 块钱就可以从火车站坐到山塘街：</p><p><img src="/posts/29a993d6/f078bd908cc433f0f84750c37001af1f.jpg" alt="行船"></p><h3 id="3-9-山塘街"><a href="#3-9-山塘街" class="headerlink" title="3.9 山塘街"></a>3.9 山塘街</h3><p>山塘街的商业化特征非常明显，我觉得不如观前街好。但是它附近的居民区倒是很自然：</p><p><img src="/posts/29a993d6/cd3afee0869d95574f081d412d1dd458.jpg" alt="山塘街"></p><hr><h2 id="四、美食"><a href="#四、美食" class="headerlink" title="四、美食"></a>四、美食</h2><h3 id="4-1-聚新春"><a href="#4-1-聚新春" class="headerlink" title="4.1 聚新春"></a>4.1 聚新春</h3><p>我们是晚上到的，在酒店磨叽了一会儿，本来说去榜上有名的同德兴吃苏式面，结果这家店七点就打烊了，我们去问得时候，还以为没有开餐呢？结果店员跟我们说，她们这边是“早七晚七”，所以已经要收拾收拾下班了。</p><p>没有办法我们只好转向旁边的聚新春，看上去也是一家特色的苏式面馆。不过其实并没有那么好吃。</p><p>我点的是焖肉虾仁双拼面。焖肉就是那种非常肥的五花肉，而且有那种炖肉的平淡的感觉。瘦肉的部分确实烂糊，但是依然有种柴的感觉，而且不香。</p><p>虾仁按照店员的说法，都是她们大早上起来手工剥的河虾，说是苏州的河多虾才能好，到别的地方可都吃不上。我倒是觉得只是普通的虾仁罢了，给得倒是不少。</p><p>苏式面的面汤倒是受到了马博的好评，我也觉得不错，只不过没有那么惊艳罢了。</p><p><img src="/posts/29a993d6/b2691460d3680787e81adfc6eeb6f08d.jpg" alt="焖肉虾仁面"></p><p>此外我们还尝试了他家的特色，紧酵馒头（名为“馒头”，是为“包子”）：</p><p><img src="/posts/29a993d6/19ea38c8d53a0304a4a119dad5b9a15f.jpg" alt="紧酵馒头"></p><p>长得确实非常可口诱人，不过吃上去就跟河南的水煎包一样，是发面的，而且是甜口的，甚至汤汁里面还带着一股菜籽油的味道，我不是很喜欢。</p><h3 id="4-2-观前街小吃"><a href="#4-2-观前街小吃" class="headerlink" title="4.2 观前街小吃"></a>4.2 观前街小吃</h3><p>观前街上的苏式小吃还是很多样的，我和马博就感觉每一个甜品店都非常香甜。</p><p>我选了一个正在做活动的血糯米吃，这个名字真的深得我心。它的口味非常独特，上面是那种蛋香味儿非常足的，中间那层奶香味也很好，底下的糯米和红豆也非常的香甜，并且不腻（它是怎么做到把这三个这么甜的东西做到一起还不腻人的）。</p><p><img src="/posts/29a993d6/7301f2240ee51900fb7d973e75ec9a84.jpg" alt="血糯米"></p><p>煎小螃蟹也是这边的特色，我第一次吃，我觉得非常好吃，但是吃多了就会有一种嚼海盐石灰的感觉。店主说这些螃蟹也是从小河里面捞上来的，先煎后炒，只可惜我不能吃太辣的。</p><p><img src="/posts/29a993d6/d9fd01f642ba8e1d0331b9c4b000cae3.jpg" alt="小螃蟹"></p><p>在观前街上有一家名叫“黄天源”的苏式点心店，是“聚新春”的服务员推荐的。它家真的是很合适的一家礼品店。下图是他家的海棠酥。在口感上跟面包差不多，但是长得真的好漂亮：</p><p><img src="/posts/29a993d6/0037323aca386af779b2e641d1c88c71.jpg" alt="黄天源"></p><p>南京的卤货也很出名呀，而且真的非常好吃，卤汁都吸得饱饱的。</p><p><img src="/posts/29a993d6/5a569c391660fe18a5aba259d796213d.jpg" alt="卤货"></p><h3 id="4-3-吴记味缘楼"><a href="#4-3-吴记味缘楼" class="headerlink" title="4.3 吴记味缘楼"></a>4.3 吴记味缘楼</h3><p>这家专门做松鼠鳜鱼的，甚至门口立着一块牌子，上面记录着他们这一个季度一共卖出了多少条松鼠鳜鱼。</p><p>老板娘非常的人美心善，她跟我们说不要点那个 188 的鳜鱼，而是要点那个 128 的松鼠鲈鱼，口味都是一样的，还更便宜。</p><p>我在很多地方都吃过松鼠鳜鱼，但是无疑这一家做得是最好吃的。很多地方的鱼在炸过以后，就只剩下那种酥皮的味道了，甚至都有些发苦了。而这家的鱼在炸完后，表皮酥，中层有一种咸蛋黄芝士一样的粉状感，内层还是大块鱼肉的紧致和鲜美。再配上这个酸甜口的酱汁（并没有番茄酱的那种𫫇酸味道），简直就是完美！</p><p><img src="/posts/29a993d6/55f17b11f4be15cf03cd52985ec36d1b.jpg" alt="松鼠鳜鱼"></p><h3 id="4-4-马头巷"><a href="#4-4-马头巷" class="headerlink" title="4.4 马头巷"></a>4.4 马头巷</h3><p>非常好的一家餐馆！非常有特色：</p><p><img src="/posts/29a993d6/cd1e3e3ec1922839a75552eefb5c17e4.jpg" alt="龙虾"></p><p>这个龙虾非常的香，哇，真的，这个红油调配的，是怎么做到，既有酒的清冽，又有麻油的刺激，又有虾黄的醇香。</p><p><img src="/posts/29a993d6/658feee4f7547107fea07b66f8d12027.jpg" alt="排骨盖饭"></p><p>这个饭比排骨好吃，有种黏糊的香味儿。</p><p>似乎苏州的鸡头米很好吃，我就尝了尝，我觉得跟莲子的口感很像，但是要比莲子更加的牙碜一些：</p><p><img src="/posts/29a993d6/50c7bf43bf5eca4d2700729ae75558b8.jpg" alt="鸡头米"></p><h3 id="4-5-苏三姑"><a href="#4-5-苏三姑" class="headerlink" title="4.5 苏三姑"></a>4.5 苏三姑</h3><p>这家店的预制菜现象很明显，点完菜不到五分钟菜就上齐了。</p><p>稻草扎肉这个名字真的很好听。但是它就跟那个焖肉面的口感很相似，都非常的腻，而且没有味道：</p><p><img src="/posts/29a993d6/a2cf80c3fa27598ec82044e205801032.jpg" alt="稻草扎肉"></p><p>这个柠檬虾很好吃呀，清爽甘洌：</p><p><img src="/posts/29a993d6/e2eb80f83c88d7aa2625bc8c871add82.jpg" alt="柠檬虾"></p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、友情&quot;&gt;&lt;a href=&quot;#一、友情&quot; class=&quot;headerlink&quot; title=&quot;一、友情&quot;&gt;&lt;/a&gt;一、友情&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;莫愁前路无知己，天下谁人不识君。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我很少谈起朋友，真的很少谈起。或许在我的潜意识里，我一直都不是一个好朋友。我的意思是，我总是不断的挣扎，直到把所有人远远的推开。但是在偶然的相遇，和必然的分离之间，依然有一些值得称道的情谊。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/29a993d6/c7f453d58fe2b6f78e2386a9f4f7595b.jpg&quot; alt=&quot;合照&quot;&gt;&lt;/p&gt;
&lt;p&gt;酸了这么多，大概只是为了说一下，这次去苏州是和马博一起去的，然后体验非常不错。跟着马博去最大的好处就是，不用动脑子想去哪里，马博都规划好了。当然最大的缺点就是，我现在完全想不起来苏州我们玩过哪里了，真的是一点也记不住了。&lt;/p&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="S11课上" scheme="https://thysrael.github.io/tags/S11%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
  </entry>
  
  <entry>
    <title>办公工具-Server Cli 工具推荐</title>
    <link href="https://thysrael.github.io/posts/2eb2eeb5/"/>
    <id>https://thysrael.github.io/posts/2eb2eeb5/</id>
    <published>2026-01-20T13:38:17.000Z</published>
    <updated>2026-01-20T14:04:29.750Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h2><p>在使用服务器等非非本地电脑的情况下，我们常常面临一个非常原始的 shell。这种原始的环境，不仅会降低开发的效率，而且还会导致操作错误的概率大大增加（比如还未激活某个 python 的虚拟环境，就进行一些包的安装等，或者在错误的路径下删除文件）。</p><p><img src="/posts/2eb2eeb5/clear.png" alt="img"></p><p>在 LLM Agent 和 VSCode 自动化的背景下，大大降低了 shell 中需要优化的 cli 工具的数量，比如说 docker，文件管理器，编译命令，下载命令，direnv，手册查询命令，git 客户端等，这些都可以很好被 Agent 或者 VSCode 代替。</p><p>在这种严苛的环境下，这对 cli 工具提供了更加严苛的要求和品味，我总结为如下几点：</p><ul><li>提高 UI 中信息的密度（Agent 确实不需要，但是人眼需要）</li><li>解决非 project 问题（project 的问题有 VSCode 和 Agent）</li><li>易于部署、维护（在网络受限和权限受限的情况下快速部署）</li><li>要不破坏原有 shell 中的传统工具和规范（避免 make 和 agent 依赖）</li><li>在使用时，不引入额外的心智负担。即，尽可能无缝加强原有的体验（这点其实与上面有些 tradeoff，功能的加强，往往意味着原有工具的语义发生了变化）。</li></ul><p>这篇文章主要有两点贡献：</p><ul><li>推荐了一系列符合上述要求的 cli 工具</li><li>实现了一个易于部署和维护的<a href="https://github.com/Thysrael/dotfiles">框架</a></li></ul><hr><h2 id="二、原有-shell-的增强"><a href="#二、原有-shell-的增强" class="headerlink" title="二、原有 shell 的增强"></a>二、原有 shell 的增强</h2><p>这章介绍的是，不需要新下载任何 cli 工具，就可以获得更良好的体验的方法。</p><h3 id="2-1-快捷键的使用"><a href="#2-1-快捷键的使用" class="headerlink" title="2.1 快捷键的使用"></a>2.1 快捷键的使用</h3><p>默认的 shell 应该使用的 emacs 键位（或者说 mac 键位）：</p><ul><li><code>Tab</code> 补全</li><li><code>C-f</code> 前进一个字符</li><li><code>C-b</code> 后退一个字符</li><li><code>C-a</code> 回到行首</li><li><code>C-e</code> 回到行尾</li><li><code>M-f</code> 前进一个单词</li><li><code>M-b</code> 后退一个单词</li></ul><p>BTW，将 <code>casplock</code> 改成“单击是 <code>Esc</code>，合击是 <code>Ctrl</code>”，是一个非常非常好的习惯。</p><h3 id="2-2-配置基础工具"><a href="#2-2-配置基础工具" class="headerlink" title="2.2 配置基础工具"></a>2.2 配置基础工具</h3><p>在 agent 和 vscode 的围剿下我们还能用到的需要配置后才能更加好用的基础工具，只有 <code>vim</code>，<code>git</code> 和 <code>tmux</code> 了。</p><p>Vim 比较有趣的点，是相对行号，行高亮，指示 mode 的光标等特性。Git 主要是需要登记自己的邮箱，全局 ignore 文件等特性。TMux 主要是对于不同 session ，window 的显示等。</p><p>这些工具一般都在服务器上有，所以我们只需要写好配置文件就可以了。</p><h3 id="2-3-Env-与-Alias"><a href="#2-3-Env-与-Alias" class="headerlink" title="2.3 Env 与 Alias"></a>2.3 Env 与 Alias</h3><p>通过修改环境变量和设置 alias ，我们也可以提高一定的工作效率，比如说：</p><pre class="line-numbers language-Bash" data-language="Bash"><code class="language-Bash"># gitalias ga="git add ."alias gc="git commit -m"alias gp="git push"alias gl="git log --graph --oneline --decorate "# tmuxalias tl="tmux ls"alias ta="tmux attach -t"alias tk="tmux kill-session -t"alias tn="tmux new -s"# proxyexport HTTPS_PROXY=http://ipads:ipads123@202.120.40.82:11235<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>总之，这些配置同样需要写到一个文件里，只是不是写到某个工具的配置文件里，而是写到 shell 的配置文件里。</p><h2 id="三、UI-信息密度的增加"><a href="#三、UI-信息密度的增加" class="headerlink" title="三、UI 信息密度的增加"></a>三、UI 信息密度的增加</h2><h3 id="3-1-Nerd-Font：带-Icon-的字体"><a href="#3-1-Nerd-Font：带-Icon-的字体" class="headerlink" title="3.1 Nerd Font：带 Icon 的字体"></a>3.1 Nerd Font：带 Icon 的字体</h3><p>传统的 icon 是一个图片，自然是无法显示在只能显示 char 的 termianl 中（现在 kitty 等先进终端到可以）。</p><p>Nerd Font 是一类特殊的 font，他们有些 char 就是 icon，因此是可以在 termial 中显示的。利用这一点，我们就可以提高 UI 的信息密度了（icon 的表达能力是 ≥ char 的，就比如 <code>≥</code> 的表达能力是大于 <code>大于等于</code> 的）。</p><h3 id="3-2-P10K：带附加信息的双行-Prompt"><a href="#3-2-P10K：带附加信息的双行-Prompt" class="headerlink" title="3.2 P10K：带附加信息的双行 Prompt"></a>3.2 P10K：带附加信息的双行 Prompt</h3><p>在 shell 的 prompt 中可以呈现许多有用的信息，比如当前的路径，所处的 host，所处的虚拟环境等。</p><p><img src="/posts/2eb2eeb5/20260120214219662.png" alt="img"></p><p>但是最重要的一点是，shell prompt 应该是双行设置，不然很容易出现信息挤压的情况。</p><p>单行：</p><p><img src="/posts/2eb2eeb5/20260120214219712.png" alt="img"></p><p>双行</p><p><img src="/posts/2eb2eeb5/20260120214219704.png" alt="img"></p><p>我目前使用 p10k，缺点是只能在 zsh 中使用，优点是速度快。</p><p>也可以使用 starship。</p><h3 id="3-3-Lsd：带-Icon-的-ls"><a href="#3-3-Lsd：带-Icon-的-ls" class="headerlink" title="3.3 Lsd：带 Icon 的 ls"></a>3.3 Lsd：带 Icon 的 ls</h3><p>Lsd 是 ls 的增强，用于给每个 item 加上图标：</p><p><img src="/posts/2eb2eeb5/20260120214219816.png" alt="img"></p><h3 id="3-4-Bat：彩色的-Cat"><a href="#3-4-Bat：彩色的-Cat" class="headerlink" title="3.4 Bat：彩色的 Cat"></a>3.4 Bat：彩色的 Cat</h3><p>提供代码高亮的 cat</p><p><img src="/posts/2eb2eeb5/20260120214220446.png" alt="img"></p><h2 id="四、非-Project-功能的增强"><a href="#四、非-Project-功能的增强" class="headerlink" title="四、非 Project 功能的增强"></a>四、非 Project 功能的增强</h2><h3 id="4-1-Zoxide：Cd-的模糊跳转"><a href="#4-1-Zoxide：Cd-的模糊跳转" class="headerlink" title="4.1 Zoxide：Cd 的模糊跳转"></a>4.1 Zoxide：Cd 的模糊跳转</h3><p>传统的 cd 往往是需要层次跳转的，也就是先跳到一个浅层目录下，然后再跳一个短距离。这主要是两点原因：</p><ul><li>人很难记住一个完整的路径（虽然可以用 tab 补全）</li><li>敲入一个完整路径太费事了。</li></ul><p>Zoxide 解决了这个问题，它维护了一个历史路径库（cache），对输入的路径进行模糊匹配，找出最有可能的路径，然后跳转过去。</p><p><img src="/posts/2eb2eeb5/20260120214219715.png" alt="img"></p><p>而路径往往具有极好的局部性，所以 zoxide 的效果非常显著。</p><h3 id="4-2-FZF：Fuzzy-Search-Everything"><a href="#4-2-FZF：Fuzzy-Search-Everything" class="headerlink" title="4.2 FZF：Fuzzy Search Everything"></a>4.2 FZF：Fuzzy Search Everything</h3><p>在命令行敲命令的时候，经常会出现有一个部分非常复杂，非常难敲的情况。</p><p>有一些复杂命令是那种敲过一遍，但是还需要继续敲的情况，这个时候我们一般会搜索历史，但是原生的搜索功能非常弱，而 FZF 强化了这一点：</p><p><img src="/posts/2eb2eeb5/20260120214219789.png" alt="img"></p><p>另一种是路径非常复杂，导致我们没法快速敲出来（我们设置都不能快速记住）</p><p><img src="/posts/2eb2eeb5/20260120214219870.png" alt="img"></p><h3 id="4-3-Zsh-AutoSuggestion：Zsh-命令的自动补全"><a href="#4-3-Zsh-AutoSuggestion：Zsh-命令的自动补全" class="headerlink" title="4.3 Zsh-AutoSuggestion：Zsh 命令的自动补全"></a>4.3 Zsh-AutoSuggestion：Zsh 命令的自动补全</h3><p>这个功能非常常见了，是 zsh 的插件，用于提示可能的历史命令：</p><p><img src="/posts/2eb2eeb5/20260120214219860.png" alt="img"></p><h2 id="五、Naive-Dotfiles-框架"><a href="#五、Naive-Dotfiles-框架" class="headerlink" title="五、Naive Dotfiles 框架"></a>五、Naive Dotfiles 框架</h2><p><a href="https://github.com/Thysrael/dotfiles">GitHub - Thysrael/dotfiles: Thysrael’s naive dotfiles.</a></p><h3 id="5-1-Tools"><a href="#5-1-Tools" class="headerlink" title="5.1 Tools"></a>5.1 Tools</h3><p>综上所述，我们需要在一个新的 server 上面部署两个东西，一个是工具的<strong>配置文件</strong>，另一个是工具的<strong>二进制文件</strong>。</p><p>配置文件的难点在于要进行版本管理，因为配置文件是需要更新的。</p><p>二进制文件的难点在于下载，因为网络可能是受限的，而且多个工具的下载也很麻烦。</p><p>NaiveDotfiles 采用如下工具实现这些功能：</p><ul><li><code>git</code>：用 git 管理配置文件</li><li><code>ln</code>：软链接将配置文件集中</li><li><code>make</code>：用 make 来整理部署脚本</li><li>Github Release + Action：预先收集所有工具的二进制包，并统一打包</li></ul><p><img src="/posts/2eb2eeb5/20260120214220014.png" alt="img"></p><h3 id="5-2-Quick-Start"><a href="#5-2-Quick-Start" class="headerlink" title="5.2 Quick Start"></a>5.2 Quick Start</h3><p>用如下命令 clone 仓库：</p><pre class="line-numbers language-Shell" data-language="Shell"><code class="language-Shell">git clone --recursive https://github.com/Thysrael/dotfiles.git<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>用如下命令下载相关的 cli 工具：</p><pre class="line-numbers language-Shell" data-language="Shell"><code class="language-Shell">./toolkit.sh<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>用如下命令部署：</p><pre class="line-numbers language-Shell" data-language="Shell"><code class="language-Shell">make server # the programs on cli servermake clean-server # remove the config<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>也可以只针对特定应用进行部署，比如说：</p><pre class="line-numbers language-Shell" data-language="Shell"><code class="language-Shell">make tmux # deploy tmux config filesmake clean-tmux # clear tmux config files<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h3 id="5-3-Limitations"><a href="#5-3-Limitations" class="headerlink" title="5.3 Limitations"></a>5.3 Limitations</h3><ul><li>Git 的信息有些是我的，需要重新配置。</li><li>如果原来就有配置文件了，使用这个框架后，原来的配置文件可能被删除。</li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、背景&quot;&gt;&lt;a href=&quot;#一、背景&quot; class=&quot;headerlink&quot; title=&quot;一、背景&quot;&gt;&lt;/a&gt;一、背景&lt;/h2&gt;&lt;p&gt;在使用服务器等非非本地电脑的情况下，我们常常面临一个非常原始的 shell。这种原始的环境，不仅会降低开发的效率，而且还会导致操作错误的概率大大增加（比如还未激活某个 python 的虚拟环境，就进行一些包的安装等，或者在错误的路径下删除文件）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/2eb2eeb5/clear.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 LLM Agent 和 VSCode 自动化的背景下，大大降低了 shell 中需要优化的 cli 工具的数量，比如说 docker，文件管理器，编译命令，下载命令，direnv，手册查询命令，git 客户端等，这些都可以很好被 Agent 或者 VSCode 代替。&lt;/p&gt;
&lt;p&gt;在这种严苛的环境下，这对 cli 工具提供了更加严苛的要求和品味，我总结为如下几点：&lt;/p&gt;</summary>
    
    
    
    <category term="办公工具" scheme="https://thysrael.github.io/categories/%E5%8A%9E%E5%85%AC%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="S11课上" scheme="https://thysrael.github.io/tags/S11%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="办公工具" scheme="https://thysrael.github.io/tags/%E5%8A%9E%E5%85%AC%E5%B7%A5%E5%85%B7/"/>
    
    <category term="工具总结" scheme="https://thysrael.github.io/tags/%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>Sys4AI-Gradient</title>
    <link href="https://thysrael.github.io/posts/495916e3/"/>
    <id>https://thysrael.github.io/posts/495916e3/</id>
    <published>2025-12-26T07:07:38.000Z</published>
    <updated>2026-01-16T12:29:50.178Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>虽然我已经在之前的文章中讨论过一遍张量是如何求导的了，并且附上了详细的数学推导。但是我觉得那次的讨论还是有些过于偏向于数学的严谨，而忽略了实际使用中的直观。</p><p>所以我打算再推一遍，省略一些数学细节，但是更加注重实际的使用，包括对于计算和内存开销的估算，矩阵的形状等。</p><hr><h2 id="二、规律"><a href="#二、规律" class="headerlink" title="二、规律"></a>二、规律</h2><h3 id="2-1-LOSS-是标量"><a href="#2-1-LOSS-是标量" class="headerlink" title="2.1 LOSS 是标量"></a>2.1 LOSS 是标量</h3><blockquote><p>无论 LLM Forward 最终是生成一个 token，还是一段 output 序列，还是一个 batch 的 output 序列组，最终的 loss 都是一个标量。</p></blockquote><p>这点似乎还是有些反直觉的，这是因为在很多科普文章中，为了简化描述，往往只会用一个 token 来举例子，而实际上，训练往往是以 batch 的形式进行，并且与不是只生成一个 token，而是一串 token。</p><p>在实际生产中，大模型 forward 的结果其实一个形状为 $[B, S, V]$ 的三维张量，用于表示概率，其中有：</p><ul><li>$B$：Batch Size</li><li>$S$：Output Sequence Length</li><li>$V$：Vocabulary Size</li></ul><p>对于每个 token 来说，它生成的是一个概率分布，形状是与词表大小相同的一维向量 ，它会与一个标量的 label（可以理解为“正确答案”）去计算交叉熵，也就是看一下这个概率分布合理与否。最终会得到一个标量的 loss 。</p><p>那么此时，我们会得到一个 loss 矩阵 $L$ ，形状是 $[B, S]$ 。问题在于，我们会直接拿着这个二维张量去进行 backward 吗？并不会，我们会对 $L$ 进行 reduce（非常简单，就是把 $L$ 的所有分量求平均值），最终得到一个标量 $l$：</p><script type="math/tex; mode=display">l = \frac{\sum^B_j \sum^S_i L_{i, j}}{BS}</script><p>至于为什么不直接用二维张量去 backward，我觉得是出于计算的简便性的考虑。我们都知道雅可比矩阵（Jacobian Matrix）是一个二维张量，而其实因变量和自变量都是一维张量。之所以会发生“升维”，是因为我们要描述“每个自变量的分量”对于“每个因变量的分量”的影响，所以维度就升高了。</p><p>如果我们直接使用 $[B, S]$ 的 $L$ 进行 backward，显然梯度的形状应该会变成高维张量，这显然不利于我们进行梯度下降。而且最关键的是，就算我们能忍受高维张量，到最终这些高维张量还是要 reduce 后再更新模型的。</p><p>也就是即使我们维护了“权重张量对于不同输出的影响”这个张量，最后我们还是要把不同输出对应的影响全加起来，然后再更新权重，相当于没变化。</p><p>总结一下，我们将上面的标量 loss 记录为 $l$ 。</p><h3 id="2-2-形状相同"><a href="#2-2-形状相同" class="headerlink" title="2.2 形状相同"></a>2.2 形状相同</h3><blockquote><p>梯度的形状是与原张量的形状是相同的。</p></blockquote><p>这个规律其实和前面的规律息息相关。</p><p>然后我们需要理解一下在 backward 中提到的“梯度下降法”的具体含义。梯度最重要的，是确定“因变量”和“自变量”。自变量很好理解，我们需要计算哪个张量的梯度，哪个张量就作为自变量。而因变量就比较迷惑了，先说结论，它一直是 $l$ 。</p><p>那么为什么说因变量比较迷惑呢？这是因为 backward 中充分应用了链式法则，在链式法则中，会引入很多的“临时梯度”，这些临时梯度的因变量不再是 $l$ 了。当这些“临时梯度”与“最终梯度”一起出现在公式中的时候，就容易让人感到迷惑了。</p><p>那么考虑一个在 LLM 中出现的张量 $X$ ，无论 $X$ 在模型的哪个位置（是最后一层，还是第一层），发挥什么作用（是 FFN 的权重，还是 Attention 的权重，还是激活值，还是偏移值），形状如何（是一维向量，还是二维矩阵，还是考虑 batch 的高维张量），它其中的每个分量都会影响 $l$ 。</p><p>所以因此我们得到的梯度 $\frac{\partial l}{\partial X}$ 的形状就一定和 $X$ 保持相同，因为它就表示了 $X$ 中的每个分量对于 $l$ 的影响。</p><p>为了强调这个规律，我们引入了新的标记：</p><script type="math/tex; mode=display">X_{G} = \frac{\partial l}{\partial X}</script><p>其中 $X_G$ 与 $X$ 的形状完全相同。</p><p>当然梯度下降法也就很好表示了，因为形状相同（这么看形状也必须相同），说白了就是：</p><script type="math/tex; mode=display">X_{new} = X_{old} - \eta X_{G}</script><h3 id="2-3-矩阵乘法很简单"><a href="#2-3-矩阵乘法很简单" class="headerlink" title="2.3 矩阵乘法很简单"></a>2.3 矩阵乘法很简单</h3><blockquote><p>对于 Forward 过程：</p><script type="math/tex; mode=display">Y = A \cdot B</script><p>无论 $A, B$ 的形状是什么，有 backward 过程：</p><script type="math/tex; mode=display">A_{G} = Y_{G} \cdot B^T</script><script type="math/tex; mode=display">B_{G} = Y_{G}^T \cdot A</script></blockquote><p>也就是说，虽然理解并推导链式法则 backward 是一件很困难的事情，但是最终的结论是非常简单且 general 的。当我们拿到一个因变量的梯度的时候，自变量的梯度计算会变得很容易记忆。</p><p>如果有两个自变量，那么某个自变量的梯度就是因变量梯度与另一个自变量的乘积（当然可能还需要转置）。</p><p>那难道 LLM 中就都是 $Y = AB$ 这种简单形式吗？难道 Attention 和 FFN 这种复杂的网络结构，也能用矩阵乘法表示吗？还真能，这是因为：</p><ul><li>我们并不限制 $A, B$ 的形状，也就是无论他们是一维的，还是二维的，上面的式子都成立。</li><li>虽然 Attention 这种结构乍一看很复杂，但是它都可以被拆成很多个矩阵乘法，比如说 $QK$ 和 $PV$ 等。</li><li>确实激活函数或者 norm 函数（包括 softmax）无法用这个模式硬套，但是用这种 vec2vec 的梯度下降，本身也不是计算的大头。</li></ul><hr><h2 id="三、应用"><a href="#三、应用" class="headerlink" title="三、应用"></a>三、应用</h2><p>在这一章里面，我们用上面介绍的规律来实战一下，其实主要就是矩阵乘法梯度的规律。</p><h3 id="3-1-FFN"><a href="#3-1-FFN" class="headerlink" title="3.1 FFN"></a>3.1 FFN</h3><p>可以被理解成两个线性映射层：</p><script type="math/tex; mode=display">Y = WX</script><p>我们已经拿到了 $Y_G$ ，带入上面的规律可知：</p><script type="math/tex; mode=display">W_G = Y_G \cdot X^T</script><script type="math/tex; mode=display">X_G = Y_G^T \cdot W</script><p>在计算 $W_G$ 的时候，需要使用到激活值 $X$，这就要求在 forward 的时候，要将这个临时的激活值进行保留。</p><p>我们计算 $W_G$ 是出于更新 $W$ 的目的，那么我们为什么要计算 $X_G$，这是因为 $X$ 此时是自变量，而它同时也是因变量，所以需要计算它保证链式传播。</p><h3 id="3-2-Attention"><a href="#3-2-Attention" class="headerlink" title="3.2 Attention"></a>3.2 Attention</h3><p>Attention 看起来很复杂，但是实际上经过拆解，并不难。在 forward 过程中，有：</p><script type="math/tex; mode=display">S = Q \cdot K^T</script><script type="math/tex; mode=display">P = Softmax(S)</script><script type="math/tex; mode=display">O = P \cdot V</script><p>经过这么一拆解，就算不算，也知道 backward 可以表示成一个很简单的形式了。</p><p>我们已经拿到了 $O_G$ ，然后有：</p><script type="math/tex; mode=display">V_G = O_G \cdot P^T</script><p>同时有：</p><script type="math/tex; mode=display">P_G = O_G^T \cdot V</script><p>这两个计算的开销是非常大的，因为我们要保留形状为 $[n, n]$ 的 $P, P_G$ （$n$ 是序列长度），这都是非常高昂的开销。</p><p>我们不考虑 $softmax$ 的梯度，但是总之 $S_G$ 的形状也是 $[n, n]$ 。</p><p>当我们有了 $S_G$ 后，我们就可以推算 $Q_G, K_G$ 了，有：</p><script type="math/tex; mode=display">Q_G = S_G \cdot K</script><script type="math/tex; mode=display">K_G = Q^T \cdot S_G</script><p>FlashAttention 之所以这么厉害，不止是因为它在 forward 的卓越贡献，能够避免 $[n, n]$ 矩阵，在 backward 中，它利用保存在 SRAM 里的 Block 形式的 $Q, K, V$ 重新算一遍 Forward，当场算出局部的 $P$，随即算出梯度，直接加和，同样不需要保存 $[n, n]$ 矩阵。</p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;虽然我已经在之前的文章中讨论过一遍张量是如何求导的了，并且附上了详细的数学推导。但是我觉得那次的讨论还是有些过于偏向于数学的严谨，而忽略了实际使用中的直观。&lt;/p&gt;
&lt;p&gt;所以我打算再推一遍，省略一些数学细节，但是更加注重实际的使用，包括对于计算和内存开销的估算，矩阵的形状等。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;二、规律&quot;&gt;&lt;a href=&quot;#二、规律&quot; class=&quot;headerlink&quot; title=&quot;二、规律&quot;&gt;&lt;/a&gt;二、规律&lt;/h2&gt;</summary>
    
    
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/categories/Sys4AI/"/>
    
    
    <category term="直观理解" scheme="https://thysrael.github.io/tags/%E7%9B%B4%E8%A7%82%E7%90%86%E8%A7%A3/"/>
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/tags/Sys4AI/"/>
    
    <category term="S11课上" scheme="https://thysrael.github.io/tags/S11%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-流浪扬州</title>
    <link href="https://thysrael.github.io/posts/dbe4a116/"/>
    <id>https://thysrael.github.io/posts/dbe4a116/</id>
    <published>2025-10-07T02:20:47.000Z</published>
    <updated>2025-10-28T01:35:46.446Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、流浪"><a href="#一、流浪" class="headerlink" title="一、流浪"></a>一、流浪</h2><blockquote><p>生活是无可避免的对抗，它是对无意义的对抗的对抗。</p><p>人一直在不可避免地流浪。</p></blockquote><p>这次国庆出游只有我一个人。排除去天津那次单纯的为了干饭，这次的扬州之行可以说是我第一次自己一个人出游。</p><p>之所以选择自己一个人，是我感觉我之前对于旅游这件事情给予了太高的期望，需要一个人破一破妄。我希望旅游能够与爱人或者朋友一起去；还希望能玩遍所有著名的景点，吃遍所有的美食；还希望能深入了解本地人，在旅游滤镜扫射不到的地方，真实的生活文化；甚至还矛盾地期望着不期而遇的浪漫。<strong>我希望通过旅游，为我无意义的生活赋予意义，对抗虚无</strong>。</p><p>冷静分析下来，这几项基本上是矛盾的，在多个人的前提下，就会出现众口难调的情况。之前的我一直不肯认清这一点，我一直在等待，等待着那个恰好满足所有条件的天赐良机的出现。</p><p>也不知道是不是因为长大了的因素，我感觉到其实本质上，即使是多个人出游，也会存在无可避免的孤独，而孤独的旅游，本质上就是流浪。那或许我可以主动选择一个人。</p><p>不过这件事情仔细想想，也很荒唐。在多人出游的情况下，那些构造出来的意义，因为不同的意志存在，是那么的庸俗和脆弱；而在单人出游的情况下，构造出的意义，是那么的封闭和意淫。<strong>在与无意义的对抗中，似乎输家永远都是我</strong>。</p><p>这次的旅游的经历似乎也暗示了这一点。无论我如何自视自己是一个品味很高，很遗世独立的人，我都需要打开大众点评，去里面寻找吃饭的地方，这当然很没有道理，因为游客去的饭店的排名是一个很遵守马太效应的事情。当一个饭店的排名变高的时候，就会吸引游客去打卡，而这种打卡又会增加饭店的排名。就算游客吃了觉得不好吃，有多少人愿意承认自己的品味欣赏不了一个排名很高的饭店呢？就算游客非常实诚，那又有多少其他的游客，会相信真的是饭店不好吃，而不是这些实诚的游客没有品味呢？</p><p>但是如果不去那些排名很高的饭店，难道要去那些排名很低的饭店吗？就算我能忍受挑错饭店的失败，那我能接受错过真正的美食的代价吗？我为了我证明我是一个很清醒的，看透了大众点评愚蠢逻辑的人，就要冒着错过一个真正美食的风险？错过真正美食，是万万不能接受的，其实也能接受，只要我把锅甩给大众点评就行了，所以我最终还是打开了大众点评。</p><p>所以最终的结果就是，我选择了一个大多数人都会选择的方案，这个方案是庸俗的。<strong>所谓的庸俗，就是无可避免的同义词</strong>。“不好不坏”、“大多数人的选择”这些词语是庸俗的特性，但不是庸俗的本质。庸俗就是“不可避免”。就是生活会堵死你的每个角落，然后告诉你，你所有企图“高雅”的行为，不过是另一种形式的“庸俗”；所有与无意义的对抗，其本身都是滑稽的，失败或许是理想主义者的勋章，但是滑稽不是；那些在海滩上堆砌的沙堡，会被海水完全吞没；用竹篮子打水，不仅打不到水，而且还会把篮子弄得更加黏腻。</p><p>事情就像那滚滚东逝的水一样，任何企图拦下它的行为，只会让它更加激荡。<strong>一切的一切，都在不可避免的走向溃败；任何企图力挽狂澜的行为，都会招致更加盛大的庸俗</strong>。底线这种东西的存在，就是为了告诉人们，突破它可能会发生一些事情，但是我们依然可以接受。而生活，与其是说对于庸俗的对抗，倒不如说是对于这种对抗本身的对抗。人无法对抗庸俗，但是可以对抗那种企图高雅（其本质是“可以避免”）的念头。</p><p>对我来说，我的一个很大的“高雅”的念头，就是我可以不用再流浪，我可以有很多人在我身边，我本想用旅游来对抗虚无。而我现在希望，要用这次的流浪，对抗这种对抗。</p><p>在去往扬州的列车上，我嗅到了汪曾祺说的和连顺的茶干的香气，听到了韦小宝的“辣块妈妈不开花”（我还搜了一下，扬州土语里是没有这句话的，“辣块”应该是“哪块”的意思，后面的就是纯粹捏造出的死妈嘴臭）的暗骂声和水银骰子的撞击声，停止了思考，然后头也不回的扎进了人头攒动的无意义洪流中。</p><hr><h2 id="二、观感"><a href="#二、观感" class="headerlink" title="二、观感"></a>二、观感</h2><p>正如前所述，我对于这次旅行是非常上心的，态度非常端正。所以可能有些美化扬州了。</p><p>国庆假期出游确实非常影响游玩体验，哪里都是一堆人磨肩接踵，照相的时候人都要从屏幕里挤出来了。更不要说稍微火一点的馆子，都要等位 100 ~ 200 桌了。</p><p>抛开这些非常主观且不可复现的因素来看，扬州文旅给我最为印象深刻的点，其实是它的“诡异”。</p><h3 id="2-1-意趣"><a href="#2-1-意趣" class="headerlink" title="2.1 意趣"></a>2.1 意趣</h3><p>一方面，扬州是我见过非常有“意趣”的一个城市。我们形容任天堂，会说“任天堂总会回应你的想象力”，这是因为任天堂的游戏总是在各种细节处反复打磨。扬州也是这样的，扬州总会回馈你的注意力，只要你漫步城市，你会发现无论你瞥到什么小东西，似乎都有一种“巧思”在里面，让你感觉：“嗯，不愧是扬州，实在是太有文化了”。这种感觉有点像是我精心配置的 emacs，可能在别人看来只是一个普通的 breadcrumb，而只有我知道，上面的 icon 是我花了多少心思做出来的。</p><p>举个例子，扬州的地砖不仅漂亮，而且各式各样，每个都非常有特色。基本上随时低头一瞅，就会感慨一下，原来还可以铺成这样：</p><p><img src="/posts/dbe4a116/微信图片_20251007192403_47_23.jpg" alt="地砖" style="zoom:20%;"></p><p>所以在这个角度上来说，我是真的很喜欢扬州。</p><h3 id="2-2-寒意"><a href="#2-2-寒意" class="headerlink" title="2.2 寒意"></a>2.2 寒意</h3><p>但是从另一个方面来说，扬州有很让我感到一种寒意。尤其是这种寒意与上文提到的意趣交织在一起的时候，就让我感到很诡异了。</p><p>一个很初见时让我觉得很有趣的点，就是只要出了扬州的景区，就会发现直接到了北方的大农村。简直完全没有过渡。下面这张照片，就是路的右侧和左侧，会发现右侧还是绿水高塔，左侧直接变成批发熟食店。右边仿佛还是烟雨江南，而左边直接变成石家庄村子了（可以看到连路灯也没有）。</p><p><img src="/posts/dbe4a116/微信图片_20251007193521_48_23.jpg" alt="微信图片_20251007193521_48_23" style="zoom:50%;"></p><p>我倒不是不能接受景区周围是农村，让我觉得诡异的是，为什么是北方农村（好歹是个水乡啊）？而且为什么要这么突然？这种感觉就像是一个在田间风吹雨打的老汉，脖子上居然是璀璨精致的钻石项链。这个老汉哪怕是带金链子呢，或者这个钻石项链哪怕是在一个满脑袋卷发棒的包租婆油腻的脖子上呢？我都可以接受的。这种反差感甚至开始让我怀疑扬州风光的真实性了。</p><p>而且这边的人的恶意也给我一种奇怪的寒意。我倒是能接受人是有恶意的，看我是一个单独的外地大学生，长得也很老实，想着宰几刀，或者糊弄一下，这是人之常情。但是扬州人的恶意，真的让我有些毛骨悚然。</p><p>举个例子，我会把共享电动车停在民宿门口，那天我推车离开的时候，就有一个在民宿门口的伺候菜地（是的，我的民宿门口是块菜地）的大娘，跟我说：“你要在这里停车？”，她的语气非常可爱和淳朴，似乎就是好奇我是如何启动这辆车的。我当时也很开心呀，说：“这个电动车真的很方便，晚上回来的时候还可以照明，黑灯瞎火的”。然后她说：“你为什么要在这里停车？”，语气依然很可爱，只是语速变快了一些，我当时一懵，然后想了想，嗷，她是不是觉得我在这里停车，占了她的门口，她是民宿的前台吗？我怎么没有见过她。所以我试着问了一下：“我是不是不可以在这里停车呀？”她回答到：“不可以。”，当时情景很诡异，就三个字，语气也很平，我也不知道该怎么接，所以我只好又问到“为什么不可以呀？”，她回答道：“车搁在这里不容易被运走。”，也没有很生气，也没有很热情，似乎只有一种嫌弃。我大概明白她的逻辑了，民宿离主干道有些远，所以如果我把车骑过来，而没有人骑回去，那么这个车可能就会滞留在这里，也没有人回收。但是我有些不明白，这是个正常现象呀，共享单车就是这样骑来骑去的呀，而且民宿门口如果不能停住户自己的车，也很奇怪啊，就算退一步来说，她真的不希望这里有车，而且不愿意付出一定的代价（比如联系一下共享单车的运营人员或者立个牌子啥的），就想折腾折腾我，也大可以大大方方地讲出来：“我是民宿的工作人员，您的行为不利于民宿的运营。”，起码不会让我感到这么奇怪。</p><p>当然也不止这一件事情，我在扬州还经历了：纪念品商店老板按着收银台算错账多收钱；餐厅服务人员把我刚吃了一口的饭，当着我的面收走；足疗店排队的时候，当着钟的面儿，撒谎快弄完了；等了一个多小时的扬州炒饭，催了一下立马甩脸子等诸多情形。这些事情其实我都能理解并且接受的，毕竟开门做生意，坑点骗点的不叫个事情；在旺期里，事情多人又累的，糊弄失误一些也是没有办法的事情。但是这些事情真的在扬州发生的时候，真的让我不寒而栗。</p><p>后来我想明白了一些，景区的人，简直和我农村家乡的人是一样的！在外面，我习惯了那种被礼貌虚伪包裹的恶意，也习惯了那种被热情和自来熟粉饰的恶意。但是我真的没有习惯我家乡的那种，笨拙、小心眼，没道理甚至还有些摆烂的恶意。这里的寒意，无论是来自市容市貌，还是人本身，都是来自我家乡农村的那种寒意。</p><p>一想到我心中江南文化，温柔乡的代表——扬州，居然毫不掩饰的存在这种故乡的气息，都觉得有些滑稽和荒唐。</p><p>当然我也要为扬州文旅辩解几句，我这次国庆出游，确实贪便宜，选的是均 300 的民宿，没有选 600 左右的正规宾馆。所以地理位置稍微偏一些（离瘦西湖只有不到 1km，只是位置靠西），服务态度稍微差一些，是可以接受的。</p><h3 id="2-3-回归"><a href="#2-3-回归" class="headerlink" title="2.3 回归"></a>2.3 回归</h3><p>不过后来的扬州双博馆之行，缓解了一部分的扬州的诡异质感。这是因为双博馆离其他景点都很远，骑电动车过去要 50 分钟左右，然后在这 50 分钟的旅程中，我发现越往双博馆去，城市的样貌越显现出来，这里也是有高楼，有小区，有路灯。游客常来的瘦西湖，东关街-皮市街，似乎是扬州的郊区。</p><p>双博馆周边就很城市化：</p><p><img src="/posts/dbe4a116/微信图片_20251007203639_49_23.jpg" alt="现代"></p><p>也挺奇怪的，我还以为扬州和南京或者西安一样，景点和市中心是重合的呢。不过细想也挺合理，西安出名的是古城，古城和现代城市重合也很正常；但是扬州出名的是瘦西湖，本质是个自然风光（其实是人工风光），那在郊区也说得过去。</p><hr><h2 id="三、地理"><a href="#三、地理" class="headerlink" title="三、地理"></a>三、地理</h2><p>这次的地理知识，关键的都来自扬州双博馆，真的是很有趣、很本分的一个博物馆。</p><h3 id="3-1-轮廓"><a href="#3-1-轮廓" class="headerlink" title="3.1 轮廓"></a>3.1 轮廓</h3><p>扬州之所以繁华，是因为在古代的时候，这里是京杭大运河的尾部，运河带动得经济蓬勃发展。而现代人们有了更高效的海运，所以扬州相比于古时候，要更加落寞一些。</p><p><img src="/posts/dbe4a116/微信图片_20251007204958_51_23.jpg" alt="地理" style="zoom:50%;"></p><p>从图中可以看到，京杭大运河是在扬州汇入长江的。严格意义上来说，扬州应该算是“江北”而非“江南”。运河从北往南穿过扬州，基本上所有的景点都在运河的西面，在东面河岸上的，只有扬州东站。</p><p>下图是历史上各个时期扬州城的变化，在最开始的时候（应该是吴越在这里）是在瘦西湖的东侧建立的，在唐朝（应该哈），扩大了版图，变得和现在的扬州市差不多了。而到了宋代的时候，扬州成了抗金的边境线，所以东北方成了前线的堡城，而西南方成了后方的大城，连接二者的走廊被称为夹城。等到了明清时候，就只剩下大城部分了，其实这样说也不严谨，应该说是靠近瘦西湖的部分，变成了富贵盐商的私人庭院，而普通百姓则聚集在了西侧。这么看，这和北京的“西富东贵”的格局也很像了。</p><p><img src="/posts/dbe4a116/微信图片_20251007204545_50_23.jpg" alt="微信图片_20251007204545_50_23" style="zoom: 67%;"></p><h3 id="3-2-景点"><a href="#3-2-景点" class="headerlink" title="3.2 景点"></a>3.2 景点</h3><p>扬州景点分布如下图所示：</p><p><img src="/posts/dbe4a116/image-20251007221059215.png" alt="景点分布"></p><p>总体来说扬州老城区里面景点最多，也最具特色，也因为分散的特点，比较适合 citywalk。不过里面的何园和个园，需要花一些时间来欣赏。</p><p>瘦西湖可以被理解成一个巨大的“皇家园林”，如果指向从南面走到北面，大概需要 3 个小时，如果希望全逛完，可能就要奔着一天去了。不过瘦西湖周边并不适合 citywalk（因为比较荒，我那张对比图就是在这附近拍摄的），只有平山堂和观音寺两个景点。</p><p>扬州双博馆在各种旅游攻略中经常被忽略，但是实际上我觉得非常好，紧挨着的美术馆也很有意思。而且正如我上面提到的，我觉得那里才是更像市中心的地方。</p><p>大运河博物馆我没有去过，就不评价了。</p><p>扬州是没有地铁的，出行主要靠腿、电动车和公交。在老城区 citywalk 非常合理，因为里面有很多的旧巷子和景点。在瘦西湖附近骑车也很合理，因为瘦西湖附近的路更稀疏一些，而且路上也不好看。公交需要提前充值，最少 10 元，我就做了两次（不过旺季车上也没什么人，还不错）。</p><h3 id="3-3-丽春院"><a href="#3-3-丽春院" class="headerlink" title="3.3 丽春院"></a>3.3 丽春院</h3><p>接下来就是最重要的环节了！让我们分析一下《鹿鼎记》中主人公、大清国一等鹿鼎公、一床七美三中的记录保持者、扬州最著名的无赖——韦小宝的故居丽春院在哪里？</p><p>首先在第二回，在韦小宝出场前，有提到丽春院是在瘦西湖畔的鸣玉坊。经过查证，鸣玉坊是金庸捏造的，但是瘦西湖畔可以保留。</p><blockquote><p>清朝康熙初年，扬州<strong>瘦西湖畔的鸣玉坊</strong>乃青楼名妓汇集之所。这日正是暮春天气，华灯初上，鸣玉坊各家院子中传出一片丝竹和欢笑之声，中间又夹着猜枚行令，唱曲闹酒，当真是笙歌处处，一片升平景象。</p></blockquote><p>其次在第三十九回，韦小宝回乡，行辕设置在了何园，而丽春院离何园不远，走一会就到了。行辕在何园肯定是金庸杜撰的，因为何园是由清光绪年间何芷舠所造，而韦小宝是康熙年间的人。不过我们得知了关键信息，就是丽春院离何园不远。</p><blockquote><p>韦小宝心想倒也有理，笑道：“依你说，那行辕设在何处才是？”那道台道：“扬州盐商有个姓何的，他家的<strong>何园</strong>，称为扬州名园第一。他有心巴结钦差大人，早就预备得妥妥贴贴，盼望大人光临。只是他功名太小，不敢出口。大人若不嫌弃，不妨移驾过去瞧瞧。”<br>这姓何的盐商家财豪富，韦小宝<strong>幼时常在他家高墙外走过</strong>，听到墙里传出丝竹之声，十分羡慕，只是从无机缘进去望上一眼，当下便道：“好啊，这就去住上几天，倘若住得不适意，咱们再搬便是。扬州盐商多，咱们挨班儿住过去，吃过去，也吃不穷了他们。”</p><p>扬州的大街小巷他无不烂熟，几乎闭了眼睛也不会走错，<strong>不多时便来到瘦西湖畔的鸣玉坊</strong>，隐隐只听得各处门户中传出箫鼓丝竹，夹着猜拳唱曲、呼幺喝六。这些声音一入耳，当真比钧天仙乐还好听十倍，心中说不出的舒服受用。走到丽春院外，但见门庭依旧，跟当年离去时并无分别。他悄悄走到院侧，推开边门，溜了进去。</p></blockquote><p>到了扬州以后，我又走访了一些扬州的本地人，他们给出的说法中有一个很有趣的，那就是丽春院就是冶春茶社。虽然我不知道这种说法是怎么起来的，但是我觉得有一定的合理性。根据资料显示，冶春茶社兴建于明末（有点子地狱笑话了，扬州十日后不可能保留），距今有 200 年的历史了，时间是对上了。</p><p>不过还有一个问题是，冶春有多家分店，到底哪个是呢？首先我们可以根据原文的描述，把那些离瘦西湖较远的都排除掉。但是还剩下三家，分别是西湖西店、西湖南店和御马头店（红色坐标是冶春茶社，蓝色的是何园）。</p><p><img src="/posts/dbe4a116/image-20251007223948357.png" alt="image-20251007223948357" style="zoom: 50%;"></p><p>到这里似乎陷入僵局了，因为我没法确定到底是哪一家，而且就算确定了，我也没有办法确定冶春茶社就是丽春院。</p><p>但是我又想了想，毕竟丽春院是杜撰的，所以肯定是没有现实对照的，我探究的目的，只是为了给丽春院找到一个现实原型，大可不必在真实性上这么较真，而是应该领会金庸的创作意图。</p><p>于是我又读了一下原文，获得了一个灵感，那就是丽春院一定不高档，但是客流量不错。这是因为高档的妓院很难容忍韦春花这种生了孩子（韦小宝）、业务能力非常糟糕（翻来覆去只会唱两三首曲子）且年老色衰的妓女。但是丽春院的客流一直不错，这从文章末韦小宝问韦春花自己的爸爸是谁时，韦春花那一连串的“报爹名”可以看出来。而其这在经典的“儿媳阿珂嫖丈母娘”的情节上也可以得到印证，如果不是一个很火的窑子，那么为什么偏偏各种人物都会聚在这里。这虽然有情节冲突设计的考量，但是丽春院客流量很大也是剧情合理的一个保证。</p><p>那么综上所述，我们得到了以下线索：</p><ol><li>丽春院在瘦西湖畔</li><li>丽春院离何园不远</li><li>丽春院的原型有可能是冶春茶社</li><li>丽春院是一个“快捷窑子”</li></ol><p>在这些线索里面，第 2 点和第 4 点有些矛盾，这是因为何园这一片乃是富贵盐商和官员的居所，在这种地方开窑子，显然是不合理的。而且第 4 点本身也很难解释，就是有什么妓院，是不高档的同时，还客流量很好呢？</p><p>这些疑问在我读到朱自清的《扬州的夏日》时，得到了灵感，里面写到：</p><blockquote><p>北门外一带，叫做下街，茶馆最多，往往一面临河，船行过时，茶客和乘客可以随便招呼说话，船上人若高兴时，也可以向茶馆中要壶茶或一两种小点心，在河中唱着、吃着、谈着，回来时再将茶壶和所谓小笼连价款一并交给茶馆中人。</p></blockquote><p>这里面对于茶馆的描述，是粗俗的，而之所以会这样，有可能是因为这些茶馆挨着运河。运河的正常运转离不开船工，这些人都是非常底层的劳动者，受教育水平不高，因此会有粗鄙的特性。而扬州又不可能剥离它们存在，这是城市繁荣不得不容忍的“代价”。如果丽春院是坐落在运河边上呢？那是不是就很合理了，它的格调自然不如那些皇亲贵胄常去的青楼高，其主要客户就是河工和船工，或者外地来的乘客，那自然客流量就会很高，而且客人也不挑剔。</p><p><img src="/posts/dbe4a116/微信图片_20251008200928_86_23.jpg" alt="冶春茶社"></p><p>当我们得到了这个猜测后，再结合第 1 和第 3 点，就会发现冶春茶社御马头店非常合适。它离西湖和何园都很近，它坐落在运河边上，繁华且世俗，只有这样的地方才会诞生韦小宝这样的扬州无赖。</p><hr><h2 id="四、景点"><a href="#四、景点" class="headerlink" title="四、景点"></a>四、景点</h2><h3 id="4-1-皮市街的巷子"><a href="#4-1-皮市街的巷子" class="headerlink" title="4.1 皮市街的巷子"></a>4.1 皮市街的巷子</h3><p>皮市街是在古代是商人贩卖皮毛的地方，现在变成了扬州第二大的商业街（第一大商业街是东关街）。</p><p>我着重逛了这条街，是因为我第一天的民宿就在皮市街旁。我在预订的时候非常开心，因为上面写的是就在皮市街旁边，我还以为花低价订到了一个商业街旁的豪华民宿呢，简直是兼顾游玩和体验风情。然后我就发现，要从皮市街到达我的民宿，需要先走一条非常狭窄的巷子（大概只有两辆电动车那么宽），然后再拐入一个只有一辆电动车那么宽的巷子，然后走到最里头，才是我的民宿，就离我不到一米的地方，就是人家居民炒菜的厨房，在民宿内，我甚至可以听到外面做菜时油点子迸溅出来的声音。</p><p><img src="/posts/dbe4a116/微信图片_20251008110307_56_23.jpg" alt="皮市街民宿" style="zoom: 25%;"></p><p>虽然我很希望我的民宿是左下角挂着红灯笼的房子，但是实际上我的民宿是右下角这个。</p><p>不过我倒没有很抵触这个事情，相反，我觉得这正是体验民俗的一个好机会，住在这里，反而脱离了传统商业街的那种预制菜的味道，更具烟火气。我觉得这边的巷子有点像电视剧里面演的北京胡同了，因为也是有些拥挤，而且生活气息和古城建筑文化很好地交织在了一起。相较于现实中的北京胡同，我觉得这边的特点是更加“干净”，虽然在这里也能看到水桶、水壶、煤气罐和晾晒的外衣内衣，但是就是要比北京胡同感觉更加整洁一些。</p><p>我猜测是因为这边的墙要比北京的更漂亮，北京的红墙还是带着一股子北方的土气，而这里的墙都是灰色的石砖垒砌的，积年累月，又有了青黑色的青苔，看上去就非常有文化氛围（即使同样是红砖垒的，这里的也比北京的好看，可以看上图左下角第二张图）。而这种斑驳的墙，削弱了其他物品的杂乱（墙就已经够乱了）。此外这边的细节也做得更好一些，比如说这里的排水渠就比北京的要更加漂亮和整洁，都是上面有盖而且不反味的，而北京的经常缺少盖子，房上的瓦一般也都是有漂亮图案的。</p><p>我是晚上到的，等我到了早晨也是一头扎进各个胡同里面，发现白天更漂亮一些，非常有感觉（我真觉得这里比北京还北京），巷子里就是普通的人家，可以看到也不是什么富贵人家（不过可能就跟北京二环似的，就算富贵我也看不出来），有普通的自行车，电动车，有的人家还养了鸽子。不过他们都尽可能地捯饬自己的门楣，走过这些巷子，你会看到非常漂亮的对联、门、花草树木和石头。石头这个肯定是精心做的，我没见过哪个北方人家，会专门摆一个像山的石头在自己门口的。</p><p><img src="/posts/dbe4a116/微信图片_20251008113239_58_23.jpg" alt="白天巷子"></p><p>不得不感慨扬州这个地方的生态确实就带这股“诗情画意”的味道，也不知道是古代的诗画家从生态中汲取了灵感，还是诗画家的灵感指导了生态的建设。我在走街串巷的时候，发现了一个荒废的院子，这如果是在石家庄，我敢肯定里面一定是充斥着碎玻璃渣子、塑料广告皮和各种猫狗人的粪便，但是这里看上去，具有有种“苔痕上阶绿，草色入帘青”的风雅感，你说气不气：</p><p><img src="/posts/dbe4a116/微信图片_20251008114037_60_23.jpg" alt="废园"></p><p>经过我不断地走街串巷，串巷走街：</p><p><img src="/posts/dbe4a116/微信图片_20251008114807_62_23.jpg" alt="走街串巷"></p><p>终于在一个非常非常深的巷子尽头，发现了让我震惊的一幕，居然在这么深的巷子里，藏着这么漂亮的一个院子！我都能想到院主在修建这个房子时开心的情绪，是不是有一种“大隐隐于市”的装逼感！</p><p><img src="/posts/dbe4a116/微信图片_20251008115102_63_23.jpg" alt="漂亮房子"></p><h3 id="4-2-皮市街"><a href="#4-2-皮市街" class="headerlink" title="4.2 皮市街"></a>4.2 皮市街</h3><p>皮市街是我见过非常优秀的商业街。不知道是不是因为我住在这里，并且在这里仔细挑选纪念品，我对于这里非常熟悉，所以没准会更加有好感一些。</p><p><img src="/posts/dbe4a116/微信图片_20251008170516_69_23.jpg" alt="皮市街"></p><p>我觉得它的优秀主要在于如下几点：</p><ol><li>没有预制美食</li><li>没有太多的预制纪念品，甚至有几家非常有特色</li><li>有很多足够出片的打卡点（虽然假期实在是人多到完全没法打卡）</li><li>因为附近巷子的加持，所以商业气息没有那么重</li><li>没有很吵闹喧哗，也没有很多的光污染</li></ol><p>只可惜实在是人太多了。</p><p>我还体验了一下扬州的足疗，这是我第一次做，感觉真的没有什么特别的，完全不惊艳：</p><p><img src="/posts/dbe4a116/微信图片_20251008182551_70_23.jpg" alt="足疗"></p><p>而且有一说一，服务态度也没有很好。</p><h3 id="4-3-何园"><a href="#4-3-何园" class="headerlink" title="4.3 何园"></a>4.3 何园</h3><p>何园被认为是晚清第一园林。也是我第一个逛的江南园林。我觉得园林的设计，就是在较为有限的空间内，将花、草、树、石、亭、房、廊、水、窗和路这些元素组合在一起，尽可能构造出更加美丽的场景。而一个元素的好看是有上限的，更体现设计思想的，其实是元素之间的组合。因此，像亭、廊、窗和路这些组合元素的设计就非常重要，他们要么作为其他元素的“画框”，要么作为其他元素的“连接器”。那么什么是好看呢？我觉得除了元素本身的美丽外，尽可能的增加组合的个数，就是好看的一种体现。</p><p>何园里的片山石房据说是石涛的孤本，大概意思就是说，乾隆年间的著名画家石涛就留下来了这么一个太湖石景观。鉴于我并不认识石涛，所以我也没有感觉有多厉害。</p><p><img src="/posts/dbe4a116/微信图片_20251008191544_78_23.jpg" alt="片石房1"></p><p>这种曲里拐弯的就是太湖石。这个片石房里有个彩蛋，就是“镜花水月”，这张图的角度看不出来，可以在下张图中看出来：</p><p><img src="/posts/dbe4a116/微信图片_20251008193520_81_23.jpg" alt="片石房2"></p><p>可以看到左边有一个镜子，据说当花开的时候，镜子里就会出现花，是为“镜中花”；而在画面的右下角，有一个圆形的倒影，是为“水中月”。</p><p>我们可以再看一个更近的水中月：</p><p><img src="/posts/dbe4a116/微信图片_20251008191545_79_23.jpg" alt="水中月"></p><p>我个人对于这种“小巧思”其实挺哭笑不得的，也不知道是不是导游编出来的。</p><p>我比较喜欢的是底下这幅，可以透过窗子看景色，窗子就像画框一样：</p><p><img src="/posts/dbe4a116/微信图片_20251008191542_76_23.jpg" alt="画框"></p><p>我个人非常喜欢何园里的廊与窗的，因为它们将各个不同的元素组合到了一起，虽然在照片上似乎并没有一个完整的景色震撼（当然也跟人多有关系！），但是现在确实非常有趣：</p><p><img src="/posts/dbe4a116/微信图片_20251008195027_83_23.jpg" alt="廊窗"></p><h3 id="4-4-个园"><a href="#4-4-个园" class="headerlink" title="4.4 个园"></a>4.4 个园</h3><p>个园的占地面积要远大于何园，不过我觉得还是何园的观感更好一些。何园的园林是一个完整的整体，回环往复，相映成趣；而个园的景致非常零散，甚至都有一种附庸风雅之感，不知道是不是因为是盐商的宅邸之故。</p><p>个园之所以以“个”为名，是因为里面有很多竹子，而多簇竹叶就会呈现“个”字的形状，我在一件店铺里找到了一个非常形象的帘子：</p><p><img src="/posts/dbe4a116/901386640ad8094e8c80c6fecc59191f.jpg" alt="个竹"></p><p>个园的南部是住宅，而北部是花园，我觉得这也是他落了下乘的地方，我觉得还是个园这种住宅和花园融为一体的设计更为得当。南部住宅非常压抑，可以看到基本上墙都是这种灰砖甚至是黑砖，连窗户都是暗棕色的，整体的饱和度非常低。而且道路非常狭窄，仅容一人通过，不知道为什么要修得这么窄。</p><p><img src="/posts/dbe4a116/2577988ffe7c119431da8cabec47f274.jpg" alt="个园住宅"></p><p>个园的竹子都是遮天蔽日的，甚至有有些阴森了：</p><p><img src="/posts/dbe4a116/d330572eab77e47fb96edd6d076c382c.jpg" alt="竹林"></p><p>我还拍了一些竹子的特写，看着也不像“个”呀哈哈哈哈，底下这幅图据说是个园的一种特色竹子，好像叫龟背竹，说是摸了可以长寿：</p><p><img src="/posts/dbe4a116/dc075cec42b98dc4aa08aceeab7f5e45.jpg" alt="竹子特写"></p><p>还有人在竹子上刻字：</p><p><img src="/posts/dbe4a116/b9016f927122de257c4ed07dd6fc0bef.jpg" alt="竹刻"></p><p>个园里面还有著名的四季石，四种不同的石头，刚好可以跟四季对上。春石形如竹笋，我觉得最妙的是，上面还有竹叶的纹路：</p><p><img src="/posts/dbe4a116/488e10ac80a2797f88de0fffd902a095.jpg" alt="春石"></p><p>夏石是太湖石，我没有太理解它跟夏天的关系是什么，我觉得可能是因为夏石紧挨着池塘，让人有种夏天池塘凉意的感觉吧：</p><p><img src="/posts/dbe4a116/c04c95c714fcdf7fd0962c09d81a314f.jpg" alt="夏石"></p><p>秋石是黄石，颜色是橙土色的，看上去就非常得秋意盎然：</p><p><img src="/posts/dbe4a116/3c11d4befc9300aa3e998baec48bb154.jpg" alt="秋石"></p><p>冬石也非常形象，灰色的石头上面覆着一层白色的石头，两种石头的颜色和材质都不同，远远看上去，就像是落满了雪的普通石头。石头后面的墙上有二十四个墙洞，据说冬天西风呼啸，吹过墙洞，变可起报春的作用：</p><p><img src="/posts/dbe4a116/aee3304ca724d26b3e27a1bc8c76a171.jpg" alt="冬石"></p><h3 id="4-5-史可法祠堂"><a href="#4-5-史可法祠堂" class="headerlink" title="4.5 史可法祠堂"></a>4.5 史可法祠堂</h3><p>从东关街走出来，往瘦西湖那边 citywalk，一路上有很多的小景点，非常的有趣，甚至记忆点还远超过个园何园这种大景点。尤其是在国庆这种背景下，著名景点都是人挤人的，而这些小景点非常的清静，能感受到一种淡季时扬州的美感。</p><p>史可法祠堂真的很漂亮，我从祠堂出来的时候，刚好看见一个大爷向着史可法的画像拜了拜：</p><p><img src="/posts/dbe4a116/a86dba7527764eb4641d8fa685da09d9.jpg" alt="史可法祠堂"></p><p>祠堂后面是一个漂亮的小公园，我感觉园林造诣真的与个园不遑多让：</p><p><img src="/posts/dbe4a116/微信图片_20251012222811_121_23.jpg" alt="祠堂公园"></p><p>出了祠堂以后我又逛到了一个寺庙，全黑的寺庙，真的非常森森寒意，有压迫感：</p><p><img src="/posts/dbe4a116/6ac9c8e7aac55957dab21071a0da7f21.jpg" alt="寺庙"></p><p>侧边也非常漂亮：</p><p><img src="/posts/dbe4a116/19b0b7e10488360184dfc3032be214e5.jpg" alt="寺庙侧边"></p><p>在寺庙旁边是郑板桥的纪念堂，只可惜里面没有什么文物：</p><p><img src="/posts/dbe4a116/ba74f5a66cab0c421763771e315b0b2b.jpg" alt="郑板桥纪念堂"></p><h3 id="4-6-观音寺"><a href="#4-6-观音寺" class="headerlink" title="4.6 观音寺"></a>4.6 观音寺</h3><p>只可惜平山堂/相国寺要门票，实在是穷得进不去了，一怒之下去了旁边的观音寺，没有想到竟然意外的漂亮和震撼。</p><p>全金的、闪耀的佛像藏在森严的、寒冷的寺庙中，这个意境太绝了：</p><p><img src="/posts/dbe4a116/56f33963044fe5b881fed7e6b8fbd9a3.jpg" alt="金观音"></p><p>还有一个非常有匠心的设计，就是寺庙看上去散发着淡淡的黄色佛光：</p><p><img src="/posts/dbe4a116/82ab0e7c3a53af29e22670d91ba336a9.jpg" alt="佛光"></p><p>实际上是因为寺庙的墙都是非常明亮的黄色，经过太阳光的反射，就形成这种淡淡的佛光。</p><p><img src="/posts/dbe4a116/a577c31925deab0a61841fa13a7346ae.jpg" alt="黄墙"></p><h3 id="4-7-文昌阁"><a href="#4-7-文昌阁" class="headerlink" title="4.7 文昌阁"></a>4.7 文昌阁</h3><p>文昌阁虽然是一个景点，但是是在环形路的中间，没法进去参观：</p><p><img src="/posts/dbe4a116/0713e8d729e8a7963e6676add97d118d.jpg" alt="文昌阁"></p><p>文昌阁再走一段就是扬州大学，既然喝了它的酸奶，还是给它留张照片吧：</p><p><img src="/posts/dbe4a116/dda89d33d385430e8d9533aaff0d1a02.jpg" alt="扬州大学"></p><p>最后放一张宋夹城，非常差评，纯公园，甚至园林都不好看，现代化和游乐园的混合体：</p><p><img src="/posts/dbe4a116/77fb60e56e6f28949eb7fb1a16ec2f09.jpg" alt="宋夹城"></p><h3 id="4-8-瘦西湖"><a href="#4-8-瘦西湖" class="headerlink" title="4.8 瘦西湖"></a>4.8 瘦西湖</h3><p>瘦西湖的门票要 100 元，还是有点小贵的。</p><p>我去的那天，扬州气温最高 38 度，所以可能我有些没有耐心了，逛得很快。我就觉得只要睁开眼，就全是漂亮的景色，但是你要说有什么非常有记忆点的，确实比较少，有一种每个景点都是 80 分的美。不过当我回看照片的时候，感觉每个都有 90 分，可能真的是因为当天的高温让人心浮气躁吧。</p><p>我想将瘦西湖这种皇家园林（没错，相比于一个自然景观，它更像是一个有围墙的园林）与个园何园这种私人园林进行一个比较。首先就是瘦西湖里面的水要更加宽阔了，是一整个湖，而不是一点小池子，是可以泛舟其上的：</p><p><img src="/posts/dbe4a116/微信图片_20251008202346_87_23.jpg" alt="湖景"></p><p>但是又说回来，瘦西湖还是太空旷了，所以有些过于静态了，以至于我看到了一股活水，都感觉非常有生命力。</p><p><img src="/posts/dbe4a116/9237ffa3ae90b61f99b6c81c5f7706b6.jpg" alt="活水"></p><p>另外园子中的动物也很多：</p><p><img src="/posts/dbe4a116/c8cc0bf49b24a332b2ea2c68940cc5d3.jpg" alt="动物"></p><p>漂亮的花树，植物也不少：</p><p><img src="/posts/dbe4a116/23a1375c5ec7681d1934a15a8b6fb971.jpg" alt="植物"></p><p>而其园子一空旷了，水榭楼台这种比较占地方的建筑才有了表现的空间：</p><p><img src="/posts/dbe4a116/71b31bea9fac882e39e6cf33d3ea2d03.jpg" alt="水榭楼台"></p><p>水多了，桥就也多了：</p><p><img src="/posts/dbe4a116/00a8b447baa0254668a92b84b18a442f.jpg" alt="桥"></p><p>当然，除了这些普通的景点外，还有著名景点，就放在下面了：</p><p><img src="/posts/dbe4a116/56f046a29566165b852d1dbf161e2603.jpg" alt="五亭桥"></p><p>白塔真是，emmm，又大又白！：</p><p><img src="/posts/dbe4a116/270b6702717217f1c80769b39c94e8c0.jpg" alt="白塔"></p><hr><h2 id="五、美食"><a href="#五、美食" class="headerlink" title="五、美食"></a>五、美食</h2><h3 id="5-1-皮市街"><a href="#5-1-皮市街" class="headerlink" title="5.1 皮市街"></a>5.1 皮市街</h3><p>皮市街的小吃真的很有特色，基本上没有那种“商业街预制小吃”，比如说长沙臭豆腐，北京糖葫芦啥的。这可能是因为扬州小吃真的种类非常繁多，各种奶制品、豆制品、饮料层出不穷。</p><p>按理说哦，我是不会买锅盔吃的，因为锅盔已经基本上算是“预制小吃”了，但是架不住小姑娘睁着圆滚滚的大眼睛提溜提溜的看我呀：</p><p><img src="/posts/dbe4a116/微信图片_20251008121017_64_23.jpg" alt="锅盔"></p><p>而且这个锅盔真的没有让人失望，我刚好买的是刚出锅的，锅盔又脆又香，咬开后馅料的香热气（这大概就是“锅气”吧）充斥整个口腔。</p><p>熏鸡腿的点排的队很长，我听锅盔老板说，每天就他们家能排起来长队，我等了 20 分钟终于获得了一只：</p><p><img src="/posts/dbe4a116/微信图片_20251008121018_65_23.jpg" alt="熏鸡腿"></p><p>果然是盛名之下无虚士，这个鸡腿最大的特点是爆汁，只要一口咬下去，那个汁水能直接把牙缝全给冲一遍。而且像这种熏鸡腿，往往在鸡皮的部分，都会有一种鸡油的腥味儿，这个非常好，完全没有。</p><p>只可惜这只鸡腿因为我喝了下面这款啤酒，被我突然其来的醉意弄掉地上了：</p><p><img src="/posts/dbe4a116/微信图片_20251008162631_66_23.jpg" alt="汉森熊"></p><p>这款汉森熊扬州菠萝啤酒真的非常非常好喝，已经算是我喝过的最好喝的啤酒了（或者如果只有 5 度的话，严格意义上讲应该是酒味饮料）。传统的啤酒总有一种发酵的酸味和苦涩味，这款啤酒使用菠萝做了一个非常好的中和，完全喝不出来异味。而且菠萝与酒混合后会有一种黏糊糊的，甜滋滋的，趋于舌头（虽然有些恶心，但是是褒义的）和果冻之间的口感，<strong>简直就像在和酒接吻一样</strong>。</p><p>不过我的酒量一如既往的稳定，在我喝了半易拉罐以后，我成功昏睡过去，并将我的大鸡腿掉在了地上。</p><h3 id="5-2-扬州大排档"><a href="#5-2-扬州大排档" class="headerlink" title="5.2 扬州大排档"></a>5.2 扬州大排档</h3><p>第一天到了以后，房东小姐姐好心的给我推荐了在皮市街南口的“大毛”、“大个子烧烤”和“大淮潮”，遗憾的是，实在是太火了，整条街都排满了，所以我只能退而求其次，选择了这家扬州大排档，但是即使落座很快，上菜依然很慢，我等了足足一个小时，足以见得扬州餐馆的火爆。</p><p>汪豆腐我似乎之前点过，之前点好像就是因为看着跟“汪曾祺”挺像的，但是没想到是“毛血旺”的意思。是豆腐和鸭血炖在一起，再在上面撒上白胡椒面。鸭血处理得不错，没有那种腥味，但是似乎也丧失了某种香味，不过这种东西怎么也不会难吃的，只是没有那么亮眼。</p><p><img src="/posts/dbe4a116/微信图片_20251008164400_67_23.jpg" alt="汪豆腐"></p><p>我还点了一份扬州炒饭，这个菜等得巨长时间，不过从后面蒋家桥的扬州炒饭对比来看，这家确实是“扬州特色炒饭”。这里面料给得很充足，而且不再有豌豆这个我觉得非常难吃的配料。吃起来米饭非常非常硬，嚼得我太阳穴生疼，而且这并不是个例，我吃完后在街上遇到一个刚从大淮潮出来的一家三口，妈妈也在吐槽这个扬州炒饭嚼得脑壳发昏。口味方面也没有很惊艳，就是普通炒饭的味道，只是更加干爽一些，各式各样的食材，并没有引入太多的杂味儿（比如说虾仁没有腥味儿，葱花没有生味儿，木耳没有水汽味儿）。</p><p>我最遗憾的事情就是，没有去更加高档的餐厅再吃一次，使我没法知道到底最好吃的扬州炒饭是什么样子的。</p><p><img src="/posts/dbe4a116/微信图片_20251008164401_68_23.jpg" alt="扬州炒饭"></p><h3 id="5-3-九炉分座"><a href="#5-3-九炉分座" class="headerlink" title="5.3 九炉分座"></a>5.3 九炉分座</h3><p>因为三春三园这样非常经典的早茶都在排大队，所以这家也是我咨询了房东小姐姐后得出的答案。</p><p>烫干丝这道菜肯定不难吃，尤其是对我一个豆腐脑袋来说。干丝蘸着下面的酱油料汁，既不腻人，也没有豆腐干常见的那种豆腥味儿。不过我其实是有点失望的，因为这个菜并没有因为把豆腐丝切得很细，而在口感上获得质变。而且烫干丝是温热的，所以即使不腻，也没有很爽口。</p><p>在吃过一次后，如果把它和黄瓜拌鸡蛋干放在一起，我大概率会选择后者。</p><p><img src="/posts/dbe4a116/微信图片_20251008183633_72_23.jpg" alt="烫干丝"></p><p>大蟹黄汤包这个非常糟糕！把这个东西做得这么大，真的只是为了美丽，没有任何的道理，完全没有办法用除了吸管，完全没有办法夹起来，所以从逻辑上来说，我就是喝了一瓶蟹汤，又吃了一个包子皮。</p><p><img src="/posts/dbe4a116/微信图片_20251008183634_73_23.jpg" alt="大蟹黄汤包"></p><p>是的，是没有馅儿的，在我不小心却必然地将汤包捅破以后，剩下的就只有一些蟹屑，而没有馅儿了：</p><p><img src="/posts/dbe4a116/微信图片_20251008183716_75_23.jpg" alt="汤包残骸"></p><p>那蟹汤和包子皮好不好吃呢？非常遗憾，并不好吃。蟹汤非常的烫嘴而且腥气，而吸管无疑又放大了这两个特性。为什么不能等凉了再吃呢？因为盛汤的是死面包子皮，不是玻璃杯，热气根本散不出去，就算凉下来了，那只会更加腥气。</p><p>死面包子皮被蟹汤泡过以后，感觉就像在吃一口痰一样，而且我觉得为了让汤包不破，它都没有被完全蒸熟就给端上来了。</p><p>三丁包（鸡丁、肉丁、笋丁）和五丁包（参丁、鸡丁、肉丁、笋丁、虾丁）都非常好吃。我觉得他们和普通包子的最大的区别，是馅儿和皮儿的交融状态更好。普通包子的馅儿往往是抱团紧实的，而皮儿是发面喧腾的，这就导致两者是非常独立的两种口感，但是这两种口感本身又都很乏味，我们包包子，不就是期望二者是融合的吗？这就是我们为什么更喜欢汤包的原因，汤包里的汤，作为中介，可以将两者联系在一起，不过在喝完汤后依然有些割裂。</p><p>三丁包和五丁包是真正做到了<strong>馅儿和皮儿的融合</strong>，从图上可以看出，它的皮儿要更加喧腾绵软，而且跟馅料也交融地更好，甚至馅儿和皮儿都没有完全的界限了，而且馅料之间也没有抱团，而是呈现一种固液混合的特点，这就比汤包里的汤要更加持久一些。</p><p>而且笋丁的清香气也很好的浸入了包子，这种清香气是由内向外的，而不是传统的蒸包子的笼屉清香的那种由外向内。</p><p><img src="/posts/dbe4a116/微信图片_20251008183631_71_23.jpg" alt="五丁包"></p><p><img src="/posts/dbe4a116/微信图片_20251008183635_74_23.jpg" alt="三丁包"></p><h3 id="5-4-大淮潮"><a href="#5-4-大淮潮" class="headerlink" title="5.4 大淮潮"></a>5.4 大淮潮</h3><p>大淮潮也是房东姐姐推荐的店，蟹粉狮子头并不好吃，感觉口感上非常的“面”和松垮，而且没有什么肉味。不过汤比较好喝，而且上面撒的豆子口感非常好，给我印象很深，应该不是豌豆，更像是荷兰豆：</p><p><img src="/posts/dbe4a116/微信图片_20251008195821_84_23.jpg" alt="蟹粉狮子头"></p><p>文思豆腐也非常常见，就，我没有喝出来它和普通的豆腐汤有什么区别。写到这里的时候其实我有点恍惚，因为我在扬州似乎吃到的东西，最后的评价都是“普通”，就是并没有很惊艳我，但是也没有很难吃。后来我想了想，觉得是因为淮扬菜的流传实在是太广了，对于一个不能吃辣的我来说，我在外面最常吃的就是淮扬菜。就拿这个文思豆腐来说，我可能之前已经吃过无数次这种比较黏稠的豆腐汤了，这里的文思豆腐，只是刀功更好了，当然就谈不上惊艳了：</p><p><img src="/posts/dbe4a116/微信图片_20251008195822_85_23.jpg" alt="文思豆腐"></p><p>有一个宫保鸡丁我没有拍照，点它的目的是为了看看是不是淮扬菜的宫保鸡丁更好吃。实际上并没有，鸡丁反而有些没有挂满酱汁，有些柴和寡淡了。不过一个比较有特点的事情是，吃了以后嘴巴会回甘，尤其是一喝茶就更加明显了。</p><p>其实我还点过一次大淮潮的外面，非常好吃。有一道虾仁瑶柱茄丁，里面的瑶柱肉嘟嘟的，非常筋道鲜嫩。其实我后来有点反应过来了，从“大淮潮”这个名字推断，似乎我应该多点一些河鲜类的菜品。另外有一道炒藕丝，也很有特点，因为藕一般是切片嘛，而且炒藕很容易炒面了，但是这道菜都没有这些缺点。BTW，藕似乎也是扬州的一个特色（也算是河鲜吧），只可惜我没有尝太多。</p><h3 id="5-4-淮扬春"><a href="#5-4-淮扬春" class="headerlink" title="5.4 淮扬春"></a>5.4 淮扬春</h3><p>这又是一家因为我早晨起不来，而且不想排队，临时选的早茶店。相比于九炉分座，淮扬春要更加平民化一些，但是不用担心它用预制菜糊弄，它非常具有烟火气，包子啥的都是需要等的。</p><p>干拌面、猪肝汤和肉馄饨是扬州人早茶的常客，但是一次都点完，还是有点太难为我的大胃袋了。干拌面就是碱水面拌酱油和香油，面条本身的口感会比较筋道，酱油香油的比例很合适，但是也没有很惊艳我，我觉得可能是因为它不是武汉热干面的那种“酱面”，不会更加下饭。猪肝汤真的是一点也不腥气（虽然我感觉“不腥”这个词在我这里都用烂了），有一个很有趣的特点，就是猪肝会被切得很薄，所以口感不会发韧，而是会发脆。肉馄饨就很普通了。</p><p><img src="/posts/dbe4a116/微信图片_20251008210124_91_23.jpg" alt="早茶"></p><p>紧接着就是我来扬州最喜欢的美食了 —— 蟹黄小笼包。注意，和上面的蟹黄大汤包完全不一样！除了做到了汤儿一点也不蟹腥这个基本点外，蟹黄和蟹肉是抱团弹牙的，蟹肉颗粒分明，而且螃蟹和猪肉的口感交织在一起，让层次更加丰富的同时，还融合出了新的味觉体验。</p><p><img src="/posts/dbe4a116/微信图片_20251008211705_104_23.jpg" alt="蟹黄小笼包"></p><h3 id="5-5-大毛"><a href="#5-5-大毛" class="headerlink" title="5.5 大毛"></a>5.5 大毛</h3><p>大毛是真的火，我晚上 8 点多去得，到的第一家，光 A 座就要等 200 桌，后来又连续换了两家（最后一家已经非常偏了），才在排了 50 多分钟以后等到入座，但是真的非常值。只是可惜因为我只有一个人，所以点一个菜我就吃饱了。</p><p>我点的是江团煲，它把江团、鸡爪和鱼豆腐炖一起。然后下酥脆的油条蘸着吃。那个油条蘸满了鱼汤儿，半边酥，半边韧，又有油香，又有鱼香。而且那个江团，贼鲜，是顺着嗓子眼滑下去；不同部位的口感也非常丰富，吃的我差点把舌头吞进去。</p><p><img src="/posts/dbe4a116/微信图片_20251008212107_105_23.jpg" alt="江团煲"></p><h3 id="5-6-扬大酸奶"><a href="#5-6-扬大酸奶" class="headerlink" title="5.6 扬大酸奶"></a>5.6 扬大酸奶</h3><p>似乎扬大在扬州就是最好的食材店的存在，我看见无论是酸奶还是奶，甚至矿泉水，都喜欢打上扬大的标签。</p><p>扬大酸奶确实是网红食品，房东小姐姐还送了我两杯，我觉得茉莉花的口味更好喝（另一个是樱花的）。酸奶入口轻轻一抿，就可以感到茉莉花花瓣，唇间还有一股子花的清香。除此之外，酸奶本身很普通。</p><p><img src="/posts/dbe4a116/dde14b1d14d0c8c0ca92f80c36fa1e88.jpg" alt="扬大酸奶"></p><h3 id="5-7-其他"><a href="#5-7-其他" class="headerlink" title="5.7 其他"></a>5.7 其他</h3><p>在出了双博馆的外面，我找到了汪曾祺的茶干，花了 10 块钱买了 6 包（唉，总感觉有些坑人），上面确实是有字的。味道嘛，就是普通的豆腐干，唯一的区别就是回甘。我还特意找了茶水来就着吃，也没有什么独特的：</p><p><img src="/posts/dbe4a116/微信图片_20251008213007_107_23.jpg" alt="茶干" style="zoom: 50%;"></p><p>想到这里有些遗憾，放一段汪曾祺的文字过过瘾：</p><blockquote><p>茶干是连万顺特制的一种豆腐干。豆腐出净渣，装在一个一个小蒲包里，包口扎紧，入锅，码好，投料，加上好抽油，上面用石头压实，文火煨煮。要煮很长时间。煮得了，再一块一块从蒲包里倒出来。这种茶干是圆形的，周围较厚，中间较薄，周身有蒲包压出来的细纹，每一块当中还带着三个字：“连万顺”，——在扎包时每一包里都放进一个小小的长方形的木牌，木牌上刻着字，木牌压在豆腐干上，字就出来了。这种茶干外皮是深紫黑色的，掰开了，里面是浅褐色的。很结实，嚼起来很有咬劲，越嚼越香，是佐茶的妙品，所以叫做“茶干”。连老大监制茶干，是很认真的。每一道工序都不许马虎。连万顺茶干的牌子闯出来了。车站、码头、茶馆、酒店都有卖的。后来竟有人专门买了到外地送人的。双黄鸭蛋、醉蟹、董糖、连万顺的茶干，凑成四色礼品，馈赠亲友，极为相宜。</p></blockquote><p>肴（xiáo）肉也是我非常想吃的一个美食，毕竟被 B 站上的《乾隆下江南》里面的肴肉馋到了。据说是因为往猪蹄肉里面加了硝盐，致使这种肉颜色有好看，肉质又紧实，还防腐，后来因为“硝”这个字不雅，就改成了“肴”。我是在蒋家桥吃的，这确实不是一个上档次的地方。整体吃下来，跟普通的火腿肉，或者卤肉也没有啥大的区别。我后来想了想，可能在古代，往肉里面加硝盐还是“无心插柳柳成荫”的行为，到了现代，硝盐已经变成了一个非常常见的食品添加剂，因为这个菜品就没有那么惊艳了（又是一个淮扬菜因为过于 popular，以至于丧失惊喜的例子）。</p><p><img src="/posts/dbe4a116/微信图片_20251008213008_108_23.jpg" alt="肴肉"></p><p>出了瘦西湖太口渴了，我在路边买了小吊柿，我发誓没有 P 图，它真的就长得这么漂亮。拧开上面的芥子，就可以直接嘬出里面的汁水，汁水非常甜，稍微有点腻人，里面还混着柿子种子，有点像奶茶珍珠的感觉。</p><p><img src="/posts/dbe4a116/微信图片_20251008213009_109_23.jpg" alt="小吊柿"></p><p>在扬州东站等车时吃的锅贴，味道本身很正常，比较有特色的点在于锅贴没有被煎得很老，以至于黏牙；而且回口有些甘甜（好像提过很多遍了），但是这个是很明显的荸荠的清甜。</p><p><img src="/posts/dbe4a116/微信图片_20251008213006_106_23.jpg" alt="锅贴"></p><hr><h2 id="六、艺术"><a href="#六、艺术" class="headerlink" title="六、艺术"></a>六、艺术</h2><h3 id="6-1-盆景"><a href="#6-1-盆景" class="headerlink" title="6.1 盆景"></a>6.1 盆景</h3><p>扬州的盆景自成一派，扬派的盆景会将枝叶扎成“云片”状，也就是非常扁平（与之形成对比的是苏派的“云朵”状）；而且讲究的是“一寸三弯”，也就是非常像太湖石一样，非常曲里拐弯：</p><p><img src="/posts/dbe4a116/微信图片_20251008204314_89_23.jpg" alt="云片"></p><p>我来扬州，其实很期待看扬州的芍药花，因为《鹿鼎记》里韦小宝就曾经拔过禅智寺的芍药。不过芍药是立夏开花，而其不像牡丹（虽然它俩长得几乎一模一样），它是草本的，所以很快就枯萎了，也就看不到了。不过我看到了非常漂亮的菊花：</p><p><img src="/posts/dbe4a116/微信图片_20251008204153_88_23.jpg" alt="菊花"></p><p>不过在盆景中，我最喜欢的山水盆景（就是其实没有什么植物，只有石头的盆景）：</p><p><img src="/posts/dbe4a116/微信图片_20251008205408_90_23.jpg" alt="山水盆景"></p><p>但似乎带植物的盆景才是妙品，因为植物同样会随着季节气候的变化而变化，也就是说，赋予了盆景更强的动感。盆景不再一直是一个样子的了。据说扬州有种紫色叶子的植物的盆景很好看，但是我去看的时候，就只有绿色叶子，就很普通，普通到我现在都记不起它的名字了。</p><h3 id="6-2-工笔"><a href="#6-2-工笔" class="headerlink" title="6.2 工笔"></a>6.2 工笔</h3><h4 id="6-2-1-静物"><a href="#6-2-1-静物" class="headerlink" title="6.2.1 静物"></a>6.2.1 静物</h4><p>之前我对于工笔其实一直没有什么好感，因为它画得再精细，也难以超越西方素描的写实。不过这次先后在徐震画展（在史可法祠堂的后面，偶然碰到的）和美术馆上的参观，让我改变了这种看法。“意趣”才是工笔的重点，也就是将对平凡的事物注入精致的视角，赋予生活独特的韵味。</p><p>比如说我最喜欢的螃蟹图，明明螃蟹就是很平常的食物，但是经过工笔的滤镜，甚至都有了一种秋天丰收的感觉。虽然西方素描静物，也应该有这种“韵味”，但是确实只有工笔才能更好地将这个情愫更加深刻得表现出来了。</p><p><img src="/posts/dbe4a116/673a7057115a398fb60bb633fe5b83a6.jpg" alt="螃蟹"></p><p>当然这个石榴图更明显，普通的石榴，通过增加裂开的部分，表达了它的成熟韵味。更妙的是，爬上果篮的螳螂，更是明示了果实的甜美诱人（旁边的柿饼也暗示了这一点），甚至增加了动态的风情：</p><p><img src="/posts/dbe4a116/b08c4cf8e6fbf20ce25f8241ef78255f.jpg" alt="石榴柿饼"></p><p>下面这幅图会更加明显一些，蟋蟀暗示了柿饼的香甜，而柿饼本身就是甜的，再加上这种暗示后，就会有种“腻人”的感觉。而旁边的茶壶和太湖石，给画面带来了清香和镇定，缓解了腻人的感觉。</p><p><img src="/posts/dbe4a116/0abd4442a72b9d30f64cc28d51de59ab.jpg" alt="蟋蟀"></p><p>当然也有没啥静物组合的巧思在里面，就是单纯好玩的，比如我最爱的大鹅：</p><p><img src="/posts/dbe4a116/8ab96b2d9986ed6b7159edd86521b990.jpg" alt="大鹅"></p><h4 id="6-2-2-现代"><a href="#6-2-2-现代" class="headerlink" title="6.2.2 现代"></a>6.2.2 现代</h4><p>除了这种比较传统的工笔之外，我还在扬州美术馆看到了将目光对焦到现代场景的工笔。不止是打破了传统的选题，还不再满足于“静物”的描写，尝试了类似于“风景画”的设计：</p><p><img src="/posts/dbe4a116/39e6ce6974c59e0f13b96e4bc1ea35dc.jpg" alt="海岸"></p><p>最让我惊喜的，是一张非常繁复，非常洛可可风格的工笔猫，也算是“中西融合”了（这顶贵妇礼帽实在是太漂亮了）：</p><p><img src="/posts/dbe4a116/0d91ecf51c62723f0517086cd4c632c1.jpg" alt="洛可可猫"></p><p>此外我还看到甚至工笔的范围不再局限于“真实”了，她开始描绘梦境和想象了。这真的有种反差的震撼，“工笔”所代表的那种“事无巨细的逼近真实与复杂的极限”的精神，与“梦境”所代表的那种“驰骋感性与想象力奔向脑海的天际线”的内核，居然能交汇在一起，也就是用显微镜记录一个虚幻的泡沫，实在是太棒了！</p><p><img src="/posts/dbe4a116/4c78f8ebb70ad73c05e51c36650822a5.jpg" alt="梦境"></p><h4 id="6-2-3-印刷"><a href="#6-2-3-印刷" class="headerlink" title="6.2.3 印刷"></a>6.2.3 印刷</h4><p>突然说起来，扬州的雕版印刷非常出名，或许扬州工笔是借鉴了版画更加有力的线条和更加开放的气象（毕竟市场说印什么就得印什么），才有了今天的卓越成绩。这里摆一张我很喜欢的版画：</p><p><img src="/posts/dbe4a116/a1c96da9395bd50c1a760cbcde9e51c1.jpg" alt="版画"></p><p>我觉得它的配色和 Kanagawa 主题非常相似，而且要更加大胆和激情：</p><p><img src="/posts/dbe4a116/kanagawa@2x.png" alt="kanagawa"></p><h3 id="6-3-扬州水墨"><a href="#6-3-扬州水墨" class="headerlink" title="6.3 扬州水墨"></a>6.3 扬州水墨</h3><h4 id="6-3-1-西方"><a href="#6-3-1-西方" class="headerlink" title="6.3.1 西方"></a>6.3.1 西方</h4><p>很多时候，我都很喜欢“徐悲鸿式”的中西结合的水墨画，这种水墨画吸取了西方绘画中的结构、透视和光影，要更加符合现代人的审美。而中国水墨画的泼墨和写意，其实是有些被西方技法限制住了，这种限制是很难突破的。</p><p>我在美术馆看到不少现代水墨作品，都是上面的思路，比如说下面这幅风景画：</p><p><img src="/posts/dbe4a116/f6a98c49f791c156a581fb52298a702a.jpg" alt="白墙"></p><p>透视和光影都很西方技法的逻辑。另外不得不感慨一句，这里的白墙看似非常“泼墨写意”，实际上非常“写实”。我估计是气候原因，扬州的有年头的白墙，真的是上面这样斑驳的感觉，不信你可以去前面翻照片。</p><p>不知道是不是在扬州这种“现实即写意”的风貌下浸润旧了，我并没有像之前一样非常欣赏这种思路下创作的画作，而是觉得这种作品有一种“匠气”，它似乎再说“诶呀，我写实又写意了，你看我好不好”。但是给我的感觉是，它没有把扬州的意趣给说透了。这点在下面这张作品中要更加明显：</p><p><img src="/posts/dbe4a116/a3a545cb2858be719d986f7017ac5751.jpg" alt="仕女图"></p><p>按理说，这里画的美人，已经是比很多工笔人物要漂亮很多了，里面有着结构正确的五官和身材，甚至还有水墨所带来的“风韵”，带式我依然觉得有些“假”。似乎那扬起的水袖，不是被风吹起的，而是塑料定型在了那里。</p><p>当然这种遗憾没有持续 1 分钟，因为我立刻就看到了更符合我那时候心里所想的画作 —— 真正的水墨。</p><h4 id="6-3-2-动物"><a href="#6-3-2-动物" class="headerlink" title="6.3.2 动物"></a>6.3.2 动物</h4><p>其实如果问我，到底什么是真正的水墨，我也不能很好的回答，但是我觉得水墨想表达的是“有意思”的感慨中的那个“意思”。这个“意思”并不是具体的细节，我们就算说一道物理题是有意思的，但是我们应该不是在说重力加速度 $g = 9.8m/s^2$ 这种具体的细节是有意思的，而是说这道题目本身是有意思的。这种东西是可以不依赖于任何真实的细节和理性，而成为人们一种“共通的语言”的。</p><p>这种东西有点像“情绪”，但是并不是情绪，而是一种与情绪正交的东西，似乎是一种脑子在运动过后的酸爽感（就像肌肉在运动过后的感觉一样）。或者是脑子运动中完成一个高难度动作的成就感。</p><p>比如这幅猫，虽然画得不如上面的工笔猫精细，跟现实中的猫也不像。但是真实的猫在面对逗猫棒时狡黠的眯眼神态，被忠实地记录了下来：</p><p><img src="/posts/dbe4a116/7c27d364c4b6ce2c054004c4f15b1456.jpg" alt="死鱼眼猫"></p><p>这幅鸟图也很有意思，眼神很像人的眼神，但是为什么要像人的眼神，那就很难思考出来了：</p><p><img src="/posts/dbe4a116/b95ede4e04485ac30c0ee2c39c362cb3.jpg" alt="鸟图1"></p><p>无独有偶：</p><p><img src="/posts/dbe4a116/54817be3c437fd15d16c3281790e72df.jpg" alt="鸟图2"></p><h4 id="6-3-3-女人"><a href="#6-3-3-女人" class="headerlink" title="6.3.3 女人"></a>6.3.3 女人</h4><p>当然了，说了这么多，就是为了给这些女性肖像水墨画铺垫。我觉得下面这幅画，就是“女人如水”的最好注脚：</p><p><img src="/posts/dbe4a116/c30fe9cf3d14a28b61c0261207cb3de0.jpg" alt="女人1"></p><p>另一幅画更有意思，里面的眼神与上面那副猫的眼神非常像，这使得这个女人带上了一种同样的狡黠和慵懒：</p><p><img src="/posts/dbe4a116/a2941f260fe56ad7669202dcf05e1d69.jpg" alt="女人2"></p><p>桌子上的静物也让人有种很安神的感觉。</p><h4 id="6-3-4-孩子们"><a href="#6-3-4-孩子们" class="headerlink" title="6.3.4 孩子们"></a>6.3.4 孩子们</h4><p>除了画家们的展品，在扬州美术馆还展览了小朋友们的画作，我看了以后大受震撼，真的，小朋友们什么时候这么厉害了。</p><p>这个素描水平，实在是不像十几岁的水平呀。</p><p><img src="/posts/dbe4a116/ddcb9f68a02ac9aac4e8103dd5a673b5.jpg" alt="佛像素描"></p><p>这个设计感，也非常棒：</p><p><img src="/posts/dbe4a116/ffcc0eb94102f6a986c45780faed3cb2.jpg" alt="凤凰"></p><p>我甚至在里面发现了 Morty：</p><p><img src="/posts/dbe4a116/526206025e01e11a5ca5ca793757378a.jpg" alt="莫蒂是你吗"></p><h3 id="6-4-工艺品"><a href="#6-4-工艺品" class="headerlink" title="6.4 工艺品"></a>6.4 工艺品</h3><p>扬州人的意趣同样贯注到了工艺品的设计上，在扬州博物馆我看到了许多非常有意思的展品。</p><p>写到这里突然感慨一下，其实我看到的最为精妙的工艺品是在故宫里的，那真的是精妙的巅峰。而扬州的工艺品，则更加的娇憨一些，别有一番风味。</p><p>拿这个举例，我记得它似乎是女子的一个发饰，这么漂亮且活灵活现的小龙虾，趴在一个女生的头发上，想一想，就很有意思。</p><p><img src="/posts/dbe4a116/dc8779ff11ab0a3621c29655dd373f32.jpg" alt="小龙虾"></p><p>这个杯子也是，虽然异型杯的设计不算是什么独特的东西，借助宝石的颜色展开雕刻也是常理，但是能将二者都落实到位，还是挺难得的。尤其是非常自然合理，谁不想饮上一口鲜花的花露呢？</p><p><img src="/posts/dbe4a116/29322c2560d26cfcebcc6e84afbceb09.jpg" alt="花杯"></p><p>这个盘子看着似乎就是普通的画，但是实际上这并不是画上去的，而是用刻刀刻上去的：</p><p><img src="/posts/dbe4a116/1e3f3cae3d6fce14db10d579f01b5faf.jpg" alt="刻盘"></p><p>扬州人在工艺品的材质上也很考究，这个上面的墨玉龙，我真的是越看越喜欢。</p><p><img src="/posts/dbe4a116/7b10400d2172322ca927f99ff09bb8ce.jpg" alt="材质"></p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、流浪&quot;&gt;&lt;a href=&quot;#一、流浪&quot; class=&quot;headerlink&quot; title=&quot;一、流浪&quot;&gt;&lt;/a&gt;一、流浪&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;生活是无可避免的对抗，它是对无意义的对抗的对抗。&lt;/p&gt;
&lt;p&gt;人一直在不可避免地流浪。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这次国庆出游只有我一个人。排除去天津那次单纯的为了干饭，这次的扬州之行可以说是我第一次自己一个人出游。&lt;/p&gt;
&lt;p&gt;之所以选择自己一个人，是我感觉我之前对于旅游这件事情给予了太高的期望，需要一个人破一破妄。我希望旅游能够与爱人或者朋友一起去；还希望能玩遍所有著名的景点，吃遍所有的美食；还希望能深入了解本地人，在旅游滤镜扫射不到的地方，真实的生活文化；甚至还矛盾地期望着不期而遇的浪漫。&lt;strong&gt;我希望通过旅游，为我无意义的生活赋予意义，对抗虚无&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;冷静分析下来，这几项基本上是矛盾的，在多个人的前提下，就会出现众口难调的情况。之前的我一直不肯认清这一点，我一直在等待，等待着那个恰好满足所有条件的天赐良机的出现。&lt;/p&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="S11课上" scheme="https://thysrael.github.io/tags/S11%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-认识自我</title>
    <link href="https://thysrael.github.io/posts/51a455b3/"/>
    <id>https://thysrael.github.io/posts/51a455b3/</id>
    <published>2025-09-14T03:05:25.000Z</published>
    <updated>2025-09-20T13:58:19.731Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>按理说，这种内容应该放到 Roam 中做一篇私人随笔，而不是作为一篇更为正式的博文。但是或许有些内容，在被人看见的时候，就能起到鼓舞作用呢。毕竟我也是被这样的文章所鼓舞。</p><p>认识自我的核心是认识到：“<strong>无论如何，只有真实的自己在那里</strong>”。</p><hr><h2 id="二、洪流"><a href="#二、洪流" class="headerlink" title="二、洪流"></a>二、洪流</h2><p>来了上海以后，认识自我的任务像洪流一样淹没了我。这主要是因为上海对于我来说太陌生了，而我在这个陌生的环境中被赋予的新角色太多了：进入新的授课体系和评价体系的一个学生和考生、在实验室从事各种科研任务的硕士生、思考自己是否要转博的决策者、思考自己研究方向的探索者、完全无法忍受上海食堂的勾芡忠实的捍卫者、失去了他的 Team 的 PM、对看垃圾的大爷和垃圾箱缺位的愤怒者、长达一个月连绵阴雨的忍受者、北方单位大院的怀念者、渴望爱情和浪漫的幻想者……</p><p>处于这些环境的我，迫不及待得想要搞清楚我该如何恰如其分的去扮演这些角色。我想要既能忠诚得履行这些角色的职责，又能为这些角色注入我的特色。所有的一切，都需要“认识自我”。只有认识了自我，才能对职责量力而行；只有认识了自我，才能恰如其分地装逼。</p><p>然后，我就崩了……</p><p>一方面，环境实在是太复杂了。从大的方面来说，我不知道 System 方向到底有多少个方向，这些方向该以何种标准去评判好坏；从小的方面说，我不知道租房子的时候，强硬的表达一个中介觉得无礼的需求，是不是会失去看上的房子。</p><p>另一方面，我离我的理想中的自己太远了。我写代码很慢、看文献很慢、笨嘴拙舌、脑子反应慢、心态还动不动老乱崩，甚至记忆力都被其他人碾压。</p><p>所以直到现在，我还处于一种懵逼的状态中，对于很多问题都没有确切的答案。虽然我作为一个非常 old school 的人，只喜欢写胜利后的总结，不喜欢写自己狼狈挣扎的时候。但是我觉得我该写一些什么了，不然我真的太容易迷失了。</p><hr><h2 id="三、自己"><a href="#三、自己" class="headerlink" title="三、自己"></a>三、自己</h2><blockquote><p>不如意事有八九，能与人言无二三。</p></blockquote><p>来到上海后，兴许是孤独和陌生所致，我很期盼能有一个“老师”去教育我。我不知道该选择哪个科研方向，不知道该以什么样的态度去面对实验室的工作，不知道一个完整的科研流程是什么样的，不知道我现在的工作属于是一个庞大机器的哪个组件，不知道为什么这个实验室在有些方面不符合我的理想，甚至不符合我的理性。我的老板从来不主动提起这些，似乎这些都是习以为常的。而有些问题，我又没有办法问出口，我不想让本来严肃的问题，沦为一种“君臣相知，其乐融融”的表演。</p><p>抛开实验室的事情不谈，我也希望有朋友或者恋人，可以跟我聊聊那些已经不适合和我妈去聊的话题。但是无一例外，都很失望。</p><p>还有些时候，我真的很在意别人的眼光，我会担心我如果做得不够优雅，就会让别人觉得我非常“孱弱”。这是我不愿意去更换 Emacs 的一个原因，我希望别人在看到我的编辑器的时候，那种由衷的赞叹：“大佬！”</p><p>只有自己可以解救自己，发现自己。</p><p>也只有自己可以承担由选择带来的后果。无论做出这个选择的人是谁，后果只有自己可以承担。</p><hr><h2 id="四、时间"><a href="#四、时间" class="headerlink" title="四、时间"></a>四、时间</h2><p>只有认识了时间，才能做到真正的耐心。</p><blockquote><p>人们总是计较一天内的成绩，而忽略一年内的改变。</p></blockquote><p>我不知道上面这句鸡汤的真假，但是为了解释“量变引起质变”，似乎也没有别的办法了。</p><hr><h2 id="五、简单"><a href="#五、简单" class="headerlink" title="五、简单"></a>五、简单</h2><p>我之前太复杂了，我想要的太多了。我想要自己有一个成熟的爱好，这个爱好既可以输入，也可以输出，既能娱乐自己，又能娱乐大众；我还希望自己不再狼狈；我还希望自己对异性有足够的吸引力；我还希望我的编程工具都是位于鄙视链的顶端并且功能非常精准；我还希望我做项目的时候非常优雅；我还希望我每天的睡眠都很好。</p><p>但是结果就是，我不仅没有做好这些，甚至我的心态还因为一次又一次的失败而经常崩溃。</p><p>少做一些，做好一部分，勇于放弃。</p><hr><h2 id="六、变化"><a href="#六、变化" class="headerlink" title="六、变化"></a>六、变化</h2><p>这个学期我经历了很多新的东西，操作系统从 Linux 换到了 Windows 又换到了 MacOS，桌面系统从 X11 换到了 Wayland，编辑器从 Emacs 换到了 NeoVIM 又换到了 VSCode，笔记软件从 Org-Roam 换到了 Obsidian。当然了，工具的变化都是小的，研究方向的改变更为显著一些。</p><p>这些变化在刚开始的时候，都是让我浑身刺痒的，感觉总是不如旧东西好。总是拼尽全力让新东西和旧东西看起来一样，让自己有一种熟悉感。而为了达成这个目标，总是要花费很多精力，而且往往还完成不了。</p><p>但是怎么说呢，我不是为了稳定而活着的，我还年轻，我还想体验新的东西，不要被吓住啊。而那些陌生的感觉，对于旧东西的怀念，是时候放下了。最关键的是，新东西之所以新，就是因为它和旧的东西不同。我不应该是为了获得一个“和旧东西完全一样”的新东西而使用新东西的。新的东西总是有趣的，open！</p><p>历史债中真正顽固的部分，只有个人观念。</p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;按理说，这种内容应该放到 Roam 中做一篇私人随笔，而不是作为一篇更为正式的博文。但是或许有些内容，在被人看见的时候，就能起到鼓舞作用呢。毕竟我也是被这样的文章所鼓舞。&lt;/p&gt;
&lt;p&gt;认识自我的核心是认识到：“&lt;strong&gt;无论如何，只有真实的自己在那里&lt;/strong&gt;”。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;二、洪流&quot;&gt;&lt;a href=&quot;#二、洪流&quot; class=&quot;headerlink&quot; title=&quot;二、洪流&quot;&gt;&lt;/a&gt;二、洪流&lt;/h2&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="直观理解" scheme="https://thysrael.github.io/tags/%E7%9B%B4%E8%A7%82%E7%90%86%E8%A7%A3/"/>
    
    <category term="S10假期" scheme="https://thysrael.github.io/tags/S10%E5%81%87%E6%9C%9F/"/>
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
  </entry>
  
  <entry>
    <title>Sys4AI-GPU</title>
    <link href="https://thysrael.github.io/posts/848ac0f9/"/>
    <id>https://thysrael.github.io/posts/848ac0f9/</id>
    <published>2025-09-13T01:38:10.000Z</published>
    <updated>2026-01-08T11:39:07.647Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、Hardware"><a href="#一、Hardware" class="headerlink" title="一、Hardware"></a>一、Hardware</h2><h3 id="1-1-SM"><a href="#1-1-SM" class="headerlink" title="1.1 SM"></a>1.1 SM</h3><p>采用我两年半前写下的博文开篇：“<strong>GPU 是一个由多个 SIMD 处理器组成的 MIMD 处理器</strong>。”</p><p>这句话的意思是说，GPU 是一个多核系统，它这里说的“核”，指的是像多核 CPU 中的 core，它对应的不是 CUDA Core，而是 SM。而 SM 本身是一个 SIMD 处理器，也就是说，SM 是一个 SIMD 处理器。CUDA Core 其实对应的是一个 ALU 。一个 SM 中有多个 CUDA Core，所以它可以用一条指令进行多个标量的计算（送入不同的 CUDA Core）。</p><p>人们常常将 CPU 比作一个无所不知的教授，GPU 比喻成成百上千个小学生。而实际上，GPU 更像是一组长着很多只不协调的手的大学生。这个比喻中，SM 对应“大学生”，而 CUDA Core 等 SM 中的计算单元对应“手”。</p><p>SM 才是指令的执行者，而不是 CUDA Core 是指令的执行者。我之所以会产生 CUDA Core 才是执行者的错觉，我猜测是因为在 SIMT 模型中，thread 对应的往往是 CUDA Core 这样的计算单元（其实也不是一一对应），而在 CPU 体系中，thread 和与之对应的 CPU Core 是指令的执行者，这就很容易让人产生，CUDA Core 才是指令的执行者的误解。</p><p><img src="/posts/848ac0f9/image-20211104155056775.png" alt="image-20211104155056775"></p><p>每个 SM 核都有自己独立的寄存器文件，L1 Cache/Shared Memory，指令调度单元等。</p><h3 id="1-2-Schedule"><a href="#1-2-Schedule" class="headerlink" title="1.2 Schedule"></a>1.2 Schedule</h3><p>现在 CPU 也有两种趋势，一个是尽可能的增加 CPU Core 的数目，另一个是尽可能的增加 CPU 的向量指令集的能力。这就导致在某种意义上来说，CPU 系统就变成了“一组长着很多只手的教授们”，和 GPU 就非常类似了。那么到底 GPU 有什么其他独特的能力呢？</p><p>我觉得有一个方面就是两者在面对 <code>ld/st</code> 访存指令导致的延迟的处理思路不同。CPU 通过构建多级 cache，来尽量降低访存指令的延迟（其实还有乱序发射）。而 GPU 并没有构建多级 cache（我猜测是因为多个核心的 cache 的硬件开销过大了），一旦遇到访存指令阻塞的情况，GPU 会立刻切换“另一个指令”来执行，充分利用那些闲置的计算资源（也就是 schedule）。这种设计思路，是一种不降低指令延迟的前提下，提高系统吞吐的方法。</p><p>那么 GPU 是如何找到那条在访存阻塞时，可以被调度填充的指令呢？如果是 CPU，CPU 会在当前线程中的后续指令里，找一条与当前指令不存在数据依赖的指令，这依赖于 scoreboard 结构。我不确定 GPU 中能不能也实现相似的功能，毕竟 scoreboard 比较复杂。但是无论如何，CPU 和 GPU 都要面对，找不到一条不存在数据依赖的指令的情况，CPU 一般就选择阻塞等待了，反正在有多级 cache 的情况下，等待时间也不会太久。而 GPU 则不行，在没有 cache 的情况下，一旦等待，那时间可就长了。所以 GPU 选择切换“线程”，从另一个“线程”中找一条指令来执行。显然两条来自不同线程的指令，之间一定是不存在数据依赖的。在 GPU 中，我们称“线程”为 warp 。</p><p>这就又引入了一大堆问题。首先，难道切换 warp 本身是没有开销的吗？在 CPU 中，切换线程虽然不用更改地址空间，但是寄存器、PC 这些上下文状态还是要借助内存来保存和恢复的，那这样开销就大了（即使对于 CPU 来说，开销也很大）。那而 GPU 的 warp 切换按理说开销也不会小，甚至更大。这是因为 SM 是一个 SIMD 处理器，涉及到的寄存器数目非常庞大，而且 GPU 的访存延迟更高。</p><p>但是实际上，warp 切换基本上是零开销的。这是因为 GPU 根本不借助内存去保存和恢复上下文；而是为每个 warp 准备单独的寄存器文件，无论这个 warp 是否活跃。所以切换 warp，就是单纯的改一下指针就好了。也就是说，虽然 GPU 的 cache 资源非常少，但是寄存器资源非常多。</p><p>这种设计更理论的来说，被称作硬件多线程（Hardware Multithreading），每个 warp 相当于是一个 SM 的硬件线程。其实这种设计在 CPU 中也有出现，被称为同步多线程（Simultaneous Multithreading, SMT），在 Intel 中被称为超线程（Hyper-Threading, HT），也就是在一个 CPU Core 中，有多份独立的寄存器文件和 PC，但是只有一份 ALU 等执行单元。HT 的表现就是“逻辑核心”数目大于“物理核心”数目。</p><p>最后再介绍一下 SM 中的 Warp Scheduler 和 Dispatch Unit。其中 Warp Scheduler 负责挑选出特定 warp 的特定指令，而 Dispatch Unit 负责将这条指令，发送（issue）给执行单元执行，这主要有两个部分，一个是选择合适的执行单元（比如整数计算就发给 CUDA Core，访存就发给访存单元），另一个是对 warp 进行一定的拆分，这是因为 warp 的数目一般是 32 ，而有些计算资源只有 8 个，那么就需要分 4 次发射。</p><h3 id="1-3-SIMT"><a href="#1-3-SIMT" class="headerlink" title="1.3 SIMT"></a>1.3 SIMT</h3><p>GPU 又在 SIMD 的基础上，实现了更为灵活的 SIMT 的抽象，这同样需要硬件的支持。SIMT 这种灵活性的意味着每个线程都可以进行 <strong>独立访存</strong> 和 <strong>独立控制流</strong> ，这两点都是 SIMD 难以进行的。</p><p>独立访存意味着每个线程都可以随机化的访问地址，而不是必须访问一组连贯的地址，牺牲的是 SIMD 整体访存的效率。在实现上，需要为每个 thread 配置一个访存单元，而如果是普通的 SIMD，其实一个 warp 配置一个访存单元就够了。</p><p>独立控制流意味着不同 thread 可以执行不同的代码，牺牲的是执行效率。在实现上，采用的是指令掩码（Mask）。</p><p>这里我们最后讨论一下 SIMT 的范围。其实很容易就会发现，warp 就是 SIMT 的范围。因为 warp 里有 32 个 thread，也就是 warp 内的每个指令，都会同时对应 32 个线程进行处理。</p><p>而如果到了软件范畴，其实 SIMT 的范围扩大了，我们使用 <code>(ctaid, tid)</code> 来完成对于 thread 的索引，当 CTA（Cooperative Thread Array）数目和 CTA 内 thread 数目增多时，SIMT 的范围就会扩大。而在底层硬件上，这些扩大的范围，最终还是会被分割成多个 warp SIMT 去执行。</p><h3 id="1-4-Memory-Hierarchy"><a href="#1-4-Memory-Hierarchy" class="headerlink" title="1.4 Memory Hierarchy"></a>1.4 Memory Hierarchy</h3><p>GPU 的 Memory Hiearchy 如下所示：</p><p><img src="/posts/848ac0f9/image-20250913163324918.png" alt="image-20250913163324918"></p><p>GPU 的 L1 Cache 在 SM 内，L2 Cache 在 GPU 片上，由所有 SM 所共享，而显存则在 GPU 片下（围绕 GPU 芯片的一堆小正方形芯片）。</p><p>从图上数据可以看出，GPU 的 Reg File 的大小是大于 L1 Cache 的。GPU 的各级 Cache 都远小于 CPU 的各级 Cache。这些现象都反应了我们前面提到的不同的设计思想。</p><p>显存在实现上是 HBM（High Bandwidth Memory）。它的带宽大约是 1,000x GB/s 量级的，这比 CPU 使用的 DDR 带宽（一般是 100x GB/s）高一个量级，是无愧 HBM 这个名字的。但是考虑到 GPU 的计算能力是 10,000x GB/s 量级的，又比 HBM 的带宽高一个量级，因此 GPU 在 LLM 任务中，往往是内存瓶颈的。另外强调，这里的的带宽，指的是将数据从显存，搬运到 GPU 上的带宽。</p><h3 id="1-5-Interconnect"><a href="#1-5-Interconnect" class="headerlink" title="1.5 Interconnect"></a>1.5 Interconnect</h3><p>一个完整的 GPU 计算节点的互联图如下所示：</p><p><img src="/posts/848ac0f9/nvidia-pascal-nvlink-power8.jpg" alt="nvidia-pascal-nvlink-power8"></p><p>可以看到，如果想要搬运数据从 CPU Memory 搬运到 GPU Memory，需要走较为缓慢的 PCIe 通路（10x GB/s），而 GPU Memory 之间的数据搬运，则可以走 NVLink（100x GB/s）。</p><p>现在的 LLM 都非常庞大，而 GPU 显存只有 10x GB 大小，所以很有可能出现显存容纳不下数据的情况。而如果我们将其 offload 到 CPU Memory 上，我们就需要忍受 PCIe 的低带宽，这甚至比 HBM 的低带宽更难以接受。</p><p>如果是分布式场景，我们一般会把参数都加载到显存后再开始任务，而不同 GPU 中的数据交换，通过 NVLink 交换，而不是用 CPU Memory 做中转（走 PCIe 太慢了）；而如果是边缘设备，我们就要想办法解决 PCIe 的瓶颈了，比如说稀疏注意力机制。</p><h3 id="1-6-Terminology"><a href="#1-6-Terminology" class="headerlink" title="1.6 Terminology"></a>1.6 Terminology</h3><p>这里整理一下 NVIDIA 和 AMD GPU 的不同术语对比：</p><div class="table-container"><table><thead><tr><th>实体</th><th>NVIDIA</th><th>AMD</th></tr></thead><tbody><tr><td>SIMD Processor</td><td>SM (Streaming Processor)</td><td>CU (Compute Unit)</td></tr><tr><td>Group of Threads</td><td>Warp</td><td>Wavefront (Wave)</td></tr><tr><td>ALU</td><td>CUDA Core</td><td>SP (Stream Processor)</td></tr><tr><td>On-chip Scratchpad</td><td>Shared Memory</td><td>LDS (Local Data Share)</td></tr><tr><td>CTA</td><td>Block Group</td><td>Work Group</td></tr><tr><td>Ecosystem</td><td>CUDA (Compute Unified Device Architecture)</td><td>ROCm (Radeon Open Compute platform)</td></tr></tbody></table></div><hr><h2 id="二、CUDA"><a href="#二、CUDA" class="headerlink" title="二、CUDA"></a>二、CUDA</h2><h3 id="2-1-CTA"><a href="#2-1-CTA" class="headerlink" title="2.1 CTA"></a>2.1 CTA</h3><p>理解 CTA 的关键，在于理解软件编程模型与硬件之间的对应关系。</p><p>是不是有了 warp 这个概念，我们剩下的事情就是在软件层面设计 warp 内的指令就够了。其实并没有，首先，warp 是局限于一个 SM 内的，而且是没有办法在不同的 SM 之间迁移的。所以如果我们希望充分利用不同的 SM，那么就要引入 CTA（Collaborative Thread Array） 的概念，一个 CTA 必须在一个特定的 SM 上，不同的 CTA 可以在不同的 SM 上，一个 SM 上可以有多个 CTA。CTA 是非常像软件 thread 的概念，一个 thread 同时仅能在一个 CPU Core 上运行，不同 thread 可以在不同的 CPU Core 上运行。有了这个概念后，我们就可以利用 <code>ctaid</code> 来在软件中使用不同的 SM，当然这种使用，有一部分是依赖于 GPU 内部的硬件调度器，这就像我们没法简单指定某个 thread 一定要与某个 CPU Core 绑定一样。</p><p>那是不是 CTA 这个“软件线程”就直接用 warp 这个“硬件线程”来一一对应就好了呢？并不是，这是因为 warp 内的 thread 数目是静态的 32 ，是不可调整的。而在软件编程中，我们希望在一个 SM 中启动的 thread 数目（也就是 CTA 中 thread 的数目）是动态的。这是因为一个 SM 内的许多资源，都不是 warp 独占，而是 warp 共享的，比如说 shared memory，两个不同的 warp 是可以利用同一片资源的。也就是说，如果 CTA 内可以包含多个 warp，那么同一个 CTA 中的不同 warp 就是可以协作的。因此，CTA 中 thread 的数目往往是 32 的倍数。</p><p>正如前所述，还有 CTA 名字中的 Collaborative 的暗示，在同一个 CTA 中的 thread 具有很好的协作性，这主要体现在两点：</p><ul><li>共享内存：如前所述，同一个 CTA 中的 thread 可以读取相同的 shared memory。</li><li>同步：同一个 CTA 内的线程可以通过 <code>__syncthreads()</code> 这样的同步指令（Barrier）来协调彼此的执行进度。不过这点有些多余，因为不同 CTA 往往是在不同 SM 上，资源竞争的可能性小了很多。</li></ul><p>在 CUDA 中，CTA 也被称作 Block，而 CTA 组被称为 Grid。如果我们希望定位到一个 thread，我们可以使用如下代码：</p><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">int idx = blockIdx.x * blockDim.x + threadIdx.x;<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>而这段代码如果对应成 PTX 代码，如下所示：</p><pre class="line-numbers language-assembly" data-language="assembly"><code class="language-assembly">mov.u32 %idx, %tid.x;          // idx = threadIdx.xmov.u32 %r2, %ntid.x;          // r2 = blockDim.xmov.u32 %r3, %ctaid.x;         // r3 = blockIdx.xmad.lo.s32 %idx, %r3, %r2, %idx; // idx = r3 * r2 + idx<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>从这点也可以看出，SIMT 模型是从硬件层面支持的，而不是用编译器，从汇编层面支持的。</p><p>我们也可以看出，我们在写 kernel 的时候，用 <code>&lt;&lt;&lt;grid, block&gt;&gt;&gt;</code> 二元组来描述并行，是非常必要的，不能只用一个一元组去描述。</p><h3 id="2-2-Grid-and-Block"><a href="#2-2-Grid-and-Block" class="headerlink" title="2.2 Grid and Block"></a>2.2 Grid and Block</h3><p>虽然已经在上一个 section 从线程协作的角度介绍过了 CTA，但是我还是希望在从编程实践的角度介绍一下 <code>grid</code> 和 <code>block</code>。</p><p>我们可以这样理解，我们为了让 GPU 的利用率变高，我们有两种思路：</p><ul><li>inter-SM：也就是要利用每一个 SM，不能有 SM 的闲置。</li><li>intra-SM：也就是要用好每一个 SM，SM 不能有闲置的资源。</li></ul><p>我们要保证这两种利用率都提高，直接说结论：Grid 越大，inter-SM 的利用率越容易高，Block 越大，intra-SM 的利用率越容易高。</p><p>考虑到并行任务的数目固定，那么 Grid 越大，则 Block 越小，inter-SM 利用率越高，intra-SM 利用率越低，反之也成立。所以我们要在 Grid 和 Block 之间做 tradeoff。</p><p>之所以有上面的结论，是因为 Block 只能在一个 SM 上运行，并且由多个 Warp 组成。所以为了提高 intra-SM 的利用率，Block 就要提高其中的 Warp 数量，这样才能在某个 warp 阻塞等待时，及时切换其他的 Warp。而为了提高 inter-SM 的利用率，Grid 里的 Block 就要足够多，才能占据全部的 SM（一个 Block 只能固定在一个 SM 上）。</p><p>至于为什么 Grid 和 Block 都是 <code>(uint, uint, uint)</code> 的三元组，这跟利用率无关，只是为了方便编程，比如在处理向量的时候，我们可以在只使用 1 个维度，而在处理视频的时候，就需要 3 个维度了。</p><h3 id="2-3-Memory-Type"><a href="#2-3-Memory-Type" class="headerlink" title="2.3 Memory Type"></a>2.3 Memory Type</h3><p>不同于简单的 CPU 编程，只用操作一种内存。当我们使用 CUDA 的时候，可以操纵多种不同类型的内存。</p><p>CUDA 中的类型如下所示：</p><p><img src="/posts/848ac0f9/memory-spaces-on-cuda-device.png" alt="Memory spaces on a CUDA device"></p><p>CUDA 使用 <code>__device__</code> 来声明一个全局变量，可以被所有 kernel 访问。如下所示：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">__device__ <span class="token keyword">unsigned</span> <span class="token keyword">long</span> <span class="token keyword">long</span> g_total_sum <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>而局部变量的使用更为复杂。我们在 kernel 中声明的局部变量，优先存放在寄存器中。而如果局部变量如果过多，就会存放到 local memory 中，这种 local memory 也是 thread 独享的。那是不是很快呢？并不是，local memory 是在显存上的一片区域，考虑到 GPU 那孱弱的 cache，local memory 的访问时延非常高。</p><p>那有没有什么时延更低的方案呢？有的，就是 Shared Memory，我们可以在局部变量前增加 <code>__shared__</code> 来表示放在共享内存中的变量：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">__global__ <span class="token keyword">void</span> <span class="token function">myKernel</span><span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>    __shared__ <span class="token keyword">float</span> shared_data<span class="token punctuation">[</span>BLOCK_SIZE<span class="token punctuation">]</span><span class="token punctuation">;</span>    <span class="token comment">// ...</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>在对共享内存进行计算之前，必须确保所有线程都已经完成了上一步的数据加载。否则，一些线程可能会读到旧的、无效的数据（竞态条件 Race Condition）。使用 <code>__syncthreads()</code> 来实现同步。</p><p>总结如下：</p><div class="table-container"><table><thead><tr><th>Memory</th><th>Location on/off chip</th><th>Cached</th><th>Access</th><th>Scope</th><th>Lifetime</th></tr></thead><tbody><tr><td>Register</td><td>On</td><td>n/a</td><td>R/W</td><td>1 thread</td><td>Thread</td></tr><tr><td>Local</td><td>Off</td><td>Yes</td><td>R/W</td><td>1 thread</td><td>Thread</td></tr><tr><td>Shared</td><td>On</td><td>n/a</td><td>R/W</td><td>All threads in block</td><td>Block</td></tr><tr><td>Global</td><td>Off</td><td>Yes</td><td>R/W</td><td>All threads + host</td><td>Host allocation</td></tr><tr><td>Constant</td><td>Off</td><td>Yes</td><td>R</td><td>All threads + host</td><td>Host allocation</td></tr><tr><td>Texture</td><td>Off</td><td>Yes</td><td>R</td><td>All threads + host</td><td>Host allocation</td></tr></tbody></table></div><h3 id="2-4-Sync-and-Stream"><a href="#2-4-Sync-and-Stream" class="headerlink" title="2.4 Sync and Stream"></a>2.4 Sync and Stream</h3><p>CPU 和 GPU 协作的方式是异步的，也就是说，CPU 在向 GPU 发送指令后，是不会等待指令返回的，它就会自己往下运行了，所以如果 CPU 想要获得 GPU 的运行结果，需要先执行同步操作：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token function">cudaDeviceSynchronize</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>不过 GPU 本身，默认是串行执行指令的，这就导致 kernel 是没有办法并行执行的，也就没有办法进行一些访存延迟隐藏之类的优化。为了改善这一点，CUDA 提出了 Stream 的概念。</p><p>在你不使用 Stream 的情况下，所有 CUDA 操作（比如在 GPU 上计算、在 CPU 和 GPU 之间拷贝数据）都默认放入一个叫做 “默认流”（Default Stream） 的大队列里。我们只需要声明多个 Stream，就可以实现并发。同一个 Stream 内的指令是顺序执行的，而不同 Stream 中的指令是可以并发执行的。</p><hr><h2 id="三、Triton"><a href="#三、Triton" class="headerlink" title="三、Triton"></a>三、Triton</h2><h3 id="3-1-Tiled-Based"><a href="#3-1-Tiled-Based" class="headerlink" title="3.1 Tiled-Based"></a>3.1 Tiled-Based</h3><p>与 CUDA 不同，Triton 并不是 SIMT 编程模型，而是 Tiled-Based 编程模型，或者换种说法，是一种 SIMD 模型。我们没有办法像在 CUDA 编程一样，操纵每个线程（或者说每个标量），我们只能操纵一个向量或者一个张量，当然了，此时他们被叫作 Tile。</p><p>在 Triton 中我们也有 grid 的概念，我们用 grid 来组织“Program Instance”，每个 Program Instance 负责一个 Tile。需要强调的是，此时的组织，不再是真的硬件映射关系了，往往只是一种算法逻辑上的分块。</p><p>（如果要深究的话，一个 Triton 的 PI，对应一到多个 CTA）。</p><p>当我们计算一个矩阵加法 $C = A + B$ 时，第 $(m, n)$ 个 PI，负责的就是第 $(m, n)$ 个 Tile 的计算。在启动核函数时，我们指定 grid 参数：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># 定义块大小 (Tile size)</span><span class="token comment"># 可以根据具体硬件和矩阵形状进行调整以获得最佳性能</span>BLOCK_SIZE_M <span class="token operator">=</span> <span class="token number">32</span>BLOCK_SIZE_N <span class="token operator">=</span> <span class="token number">32</span>    <span class="token comment"># 定义 grid，这里是关键！</span><span class="token comment"># 使用 triton.cdiv (ceiling division) 来确保所有元素都被覆盖</span>grid_m <span class="token operator">=</span> triton<span class="token punctuation">.</span>cdiv<span class="token punctuation">(</span>M<span class="token punctuation">,</span> BLOCK_SIZE_M<span class="token punctuation">)</span>grid_n <span class="token operator">=</span> triton<span class="token punctuation">.</span>cdiv<span class="token punctuation">(</span>N<span class="token punctuation">,</span> BLOCK_SIZE_N<span class="token punctuation">)</span><span class="token comment"># 将 grid 定义为一个二元组</span>grid <span class="token operator">=</span> <span class="token punctuation">(</span>grid_m<span class="token punctuation">,</span> grid_n<span class="token punctuation">)</span>    <span class="token comment"># 启动核函数</span>add_kernel<span class="token punctuation">[</span>grid<span class="token punctuation">]</span><span class="token punctuation">(</span>    a<span class="token punctuation">,</span> b<span class="token punctuation">,</span> c<span class="token punctuation">,</span>                       <span class="token comment"># 指针</span>    M<span class="token punctuation">,</span> N<span class="token punctuation">,</span>                          <span class="token comment"># 维度</span>    a<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> a<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token comment"># 矩阵 A 的步长</span>    b<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> b<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token comment"># 矩阵 B 的步长</span>    c<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span> c<span class="token punctuation">.</span>stride<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token comment"># 矩阵 C 的步长</span>    BLOCK_SIZE_M<span class="token operator">=</span>BLOCK_SIZE_M<span class="token punctuation">,</span>     <span class="token comment"># constexpr 参数</span>    BLOCK_SIZE_N<span class="token operator">=</span>BLOCK_SIZE_N<span class="token punctuation">,</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>而在算子内部，我们使用 <code>program_id</code> 获得 grid 参数：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># 1. 使用二维 program_id 获取当前程序实例负责的块的索引</span><span class="token comment"># axis=0 对应 grid 的第一个维度 (行方向)</span>pid_m <span class="token operator">=</span> tl<span class="token punctuation">.</span>program_id<span class="token punctuation">(</span>axis<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token comment"># axis=1 对应 grid 的第二个维度 (列方向)</span>pid_n <span class="token operator">=</span> tl<span class="token punctuation">.</span>program_id<span class="token punctuation">(</span>axis<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>那么在 Tiled-Based 的编程模型下，我们是如何操作数据的呢？答案是我们使用“offsets 张量”。offset 可以理解为一个标量数据的地址，而一个 offsets 张量，就可以理解为一组数据的地址。我们用一个 numpy-like 的方法表示这组 offset，如下所示：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># 2. 计算当前块的二维偏移量</span><span class="token comment"># 首先，计算 M 维度 (行) 的偏移量向量</span><span class="token comment"># tl.arange 生成 [0, 1, 2, ..., BLOCK_SIZE_M-1]</span>offs_m <span class="token operator">=</span> pid_m <span class="token operator">*</span> BLOCK_SIZE_M <span class="token operator">+</span> tl<span class="token punctuation">.</span>arange<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> BLOCK_SIZE_M<span class="token punctuation">)</span><span class="token comment"># 然后，计算 N 维度 (列) 的偏移量向量</span>offs_n <span class="token operator">=</span> pid_n <span class="token operator">*</span> BLOCK_SIZE_N <span class="token operator">+</span> tl<span class="token punctuation">.</span>arange<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> BLOCK_SIZE_N<span class="token punctuation">)</span><span class="token comment"># 3. 计算加载/存储数据的完整二维指针偏移量</span><span class="token comment">#    利用广播机制 (broadcasting) 将一维的行、列偏移量扩展成二维</span><span class="token comment">#    offs_m[:, None] -&gt; [BLOCK_SIZE_M, 1]</span><span class="token comment">#    offs_n[None, :] -&gt; [1, BLOCK_SIZE_N]</span><span class="token comment">#    相加后得到一个 [BLOCK_SIZE_M, BLOCK_SIZE_N] 的偏移矩阵</span>a_offsets <span class="token operator">=</span> a_ptr <span class="token operator">+</span> <span class="token punctuation">(</span>offs_m<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token boolean">None</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_am <span class="token operator">+</span> offs_n<span class="token punctuation">[</span><span class="token boolean">None</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_an<span class="token punctuation">)</span>b_offsets <span class="token operator">=</span> b_ptr <span class="token operator">+</span> <span class="token punctuation">(</span>offs_m<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token boolean">None</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_bm <span class="token operator">+</span> offs_n<span class="token punctuation">[</span><span class="token boolean">None</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_bn<span class="token punctuation">)</span>c_offsets <span class="token operator">=</span> c_ptr <span class="token operator">+</span> <span class="token punctuation">(</span>offs_m<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token boolean">None</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_cm <span class="token operator">+</span> offs_n<span class="token punctuation">[</span><span class="token boolean">None</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token operator">*</span> stride_cn<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以看到在上面式子中，我们得到了最终我们分别得到了 a_tile, b_tile, c_tile 对应的 offsets 张量，他们的形状都是 tile 的形状，也就是 <code>[BLOCK_SIZE_M, BLOCK_SIZE_N]</code> 。</p><p>当然，在编程中，我们也有一些边界情况，或者控制分支需要处理。在 CUDA 中，我们可以随意使用 <code>if-else</code> 这种条件判断，毕竟我们提供的是 SIMT 抽象，但是在 Triton 中，我们并不能进行分支判断，所以我们利用了 mask 张量，如下所示：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># 4. 创建二维掩码 (mask) 以处理边界情况</span><span class="token comment">#    防止因矩阵尺寸不是块大小的整数倍而导致的越界访存</span>mask_m <span class="token operator">=</span> offs_m <span class="token operator">&lt;</span> Mmask_n <span class="token operator">=</span> offs_n <span class="token operator">&lt;</span> N<span class="token comment"># 使用广播和逻辑与操作合并成二维掩码</span>mask <span class="token operator">=</span> mask_m<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token boolean">None</span><span class="token punctuation">]</span> <span class="token operator">&amp;</span> mask_n<span class="token punctuation">[</span><span class="token boolean">None</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>当我们拿到这些 offsets 张量和 mask 张量后，我们就可以在上面应用算子了，比如说 <code>load, store, dot, +</code> 等：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment"># 5. 安全地加载数据块</span><span class="token comment">#    mask=mask 确保只加载有效区域的数据</span><span class="token comment">#    other=0.0 指定在掩码为 False 的位置加载 0.0，避免计算错误</span>a_tile <span class="token operator">=</span> tl<span class="token punctuation">.</span>load<span class="token punctuation">(</span>a_offsets<span class="token punctuation">,</span> mask<span class="token operator">=</span>mask<span class="token punctuation">,</span> other<span class="token operator">=</span><span class="token number">0.0</span><span class="token punctuation">)</span>b_tile <span class="token operator">=</span> tl<span class="token punctuation">.</span>load<span class="token punctuation">(</span>b_offsets<span class="token punctuation">,</span> mask<span class="token operator">=</span>mask<span class="token punctuation">,</span> other<span class="token operator">=</span><span class="token number">0.0</span><span class="token punctuation">)</span><span class="token comment"># 6. 执行核心计算</span>c_tile <span class="token operator">=</span> a_tile <span class="token operator">+</span> b_tile<span class="token comment"># 7. 将结果安全地写回</span>tl<span class="token punctuation">.</span>store<span class="token punctuation">(</span>c_offsets<span class="token punctuation">,</span> c_tile<span class="token punctuation">,</span> mask<span class="token operator">=</span>mask<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、Hardware&quot;&gt;&lt;a href=&quot;#一、Hardware&quot; class=&quot;headerlink&quot; title=&quot;一、Hardware&quot;&gt;&lt;/a&gt;一、Hardware&lt;/h2&gt;&lt;h3 id=&quot;1-1-SM&quot;&gt;&lt;a href=&quot;#1-1-SM&quot; class=&quot;headerlink&quot; title=&quot;1.1 SM&quot;&gt;&lt;/a&gt;1.1 SM&lt;/h3&gt;&lt;p&gt;采用我两年半前写下的博文开篇：“&lt;strong&gt;GPU 是一个由多个 SIMD 处理器组成的 MIMD 处理器&lt;/strong&gt;。”&lt;/p&gt;
&lt;p&gt;这句话的意思是说，GPU 是一个多核系统，它这里说的“核”，指的是像多核 CPU 中的 core，它对应的不是 CUDA Core，而是 SM。而 SM 本身是一个 SIMD 处理器，也就是说，SM 是一个 SIMD 处理器。CUDA Core 其实对应的是一个 ALU 。一个 SM 中有多个 CUDA Core，所以它可以用一条指令进行多个标量的计算（送入不同的 CUDA Core）。&lt;/p&gt;
&lt;p&gt;人们常常将 CPU 比作一个无所不知的教授，GPU 比喻成成百上千个小学生。而实际上，GPU 更像是一组长着很多只不协调的手的大学生。这个比喻中，SM 对应“大学生”，而 CUDA Core 等 SM 中的计算单元对应“手”。&lt;/p&gt;</summary>
    
    
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/categories/Sys4AI/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/tags/Sys4AI/"/>
    
    <category term="S10假期" scheme="https://thysrael.github.io/tags/S10%E5%81%87%E6%9C%9F/"/>
    
  </entry>
  
  <entry>
    <title>计算机系统-指标</title>
    <link href="https://thysrael.github.io/posts/7e0ffd77/"/>
    <id>https://thysrael.github.io/posts/7e0ffd77/</id>
    <published>2025-09-11T10:53:48.000Z</published>
    <updated>2025-09-11T14:08:37.424Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>在形形色色的系统设计中，无论是嵌入式系统、个人桌面系统、分布式系统还是 LLM 推理引擎系统，通常有一些非常恒定的指标，或者说追求的目标。而且为了达到这些指标，采用的方法论甚至都是类似的。</p><p><img src="/posts/7e0ffd77/image-20250102194609112.png" alt="image-20250102194609112"></p><p>我想在这篇文章，去记录一下系统中涉及的那些指标，以及常见的方法论，进而分析一下这些指标的本质。</p><hr><h2 id="二、Throughput"><a href="#二、Throughput" class="headerlink" title="二、Throughput"></a>二、Throughput</h2><h3 id="2-1-降低-Latency"><a href="#2-1-降低-Latency" class="headerlink" title="2.1 降低 Latency"></a>2.1 降低 Latency</h3><p>降低 latency 是非常直观提高吞吐的一种方式。只要对于单个任务的处理时延降低了，那么单位时间内能处理的任务就会增加。</p><p>但是这种方式并没有什么好讨论的必要，很多时候，我们优化吞吐量，基于的前提都是，单个任务的时延是不可改变的。</p><h3 id="2-2-Batch"><a href="#2-2-Batch" class="headerlink" title="2.2 Batch"></a>2.2 Batch</h3><p>虽然处理一个任务的时延不可改变，但是我们可以通过同时处理多个任务的方式，来提高吞吐。</p><p>批处理本质是并行的一种子类，因为不同任务之间完全没有依赖，所以这是最为简单的一种并行。只要系统资源足够，那么批处理就可以大大提高吞吐。</p><hr><h2 id="三、Latency"><a href="#三、Latency" class="headerlink" title="三、Latency"></a>三、Latency</h2><h3 id="3-1-本质"><a href="#3-1-本质" class="headerlink" title="3.1 本质"></a>3.1 本质</h3><p>在一个多任务系统中，一个任务的总时延可以分为两个部分：</p><ul><li>排队时延</li><li>处理时延</li></ul><p>针对这两种不同的时延，我们有不同的方法。</p><p>降低时延的方法论，往往是通过<strong>提高瓶颈资源的利用率</strong>的方式来实现的。</p><h3 id="3-2-Schedule"><a href="#3-2-Schedule" class="headerlink" title="3.2 Schedule"></a>3.2 Schedule</h3><p>调度不同任务的执行顺序，可以有效的减少任务的排队时延。</p><p>调度有的时候也会改善吞吐。但是仔细思考就会发现，如果计算资源一直满负荷运行（这本质上是处理时延无法再压缩了），无论怎么调度，完成一批任务所需要的时间都是相同的，也就是吞吐并不会变化。</p><p>在实践中，调度改善吞吐，是因为有些资源被闲置了，而调度可以更加充分的利用这些闲置的资源。这本质上是缩短处理时延的一种体现。</p><h3 id="3-3-Asynchronous"><a href="#3-3-Asynchronous" class="headerlink" title="3.3 Asynchronous"></a>3.3 Asynchronous</h3><p>异步同样也是改善处理时延的一种方式。任务通常由很多个存在依赖、计算资源利用率不同的子任务组成的。如果能够打破“串行执行子任务”的思维定势，将没有依赖的子任务异步执行，减少忙等时间，提高资源的利用率。那么就可以降低时延。</p><h3 id="3-4-Speculation"><a href="#3-4-Speculation" class="headerlink" title="3.4 Speculation"></a>3.4 Speculation</h3><p>投机是一种在时间维度上预先完成子任务，甚至是减少子任务数量的数量。为了实现投机，必须能够预测任务的行为，这就要求我们深入考量任务的计算特性。</p><p>Prefetch 就是常见的投机手段。</p><p>其实在某种意义上，cache 也是一种投机手段，它通过利用任务的局部性（locality）来实现投机，减少数据的搬运。</p><h3 id="3-5-Pool"><a href="#3-5-Pool" class="headerlink" title="3.5 Pool"></a>3.5 Pool</h3><p>池化从两个方面改善时延。一方面，池化将资源变得更加“细粒度”，因此气泡会减少，资源利用率会提高。</p><p>另一方面，池化会将许多不同任务的相同子任务“合并同类项”，比如将所有线程都初始化好，等待处理任务。</p><p>在某种程度上来说，分页式的虚拟内存，都可以理解成一种对于物理内存的池化管理。</p><h3 id="3-6-Trading"><a href="#3-6-Trading" class="headerlink" title="3.6 Trading"></a>3.6 Trading</h3><p>比较直观的，就是”以存代算“和”以算代存“两种。Trading 的本质，是用非瓶颈的资源，去交换那些瓶颈的资源。</p><p>交换的方式也可以比较隐晦，比如说”近存计算“，就是将原本的数据搬运改成了控制流搬运。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;在形形色色的系统设计中，无论是嵌入式系统、个人桌面系统、分布式系统还是 LLM 推理引擎系统，通常有一些非常恒定的指标，或者说追求的目标。而且为了达到这些指标，采用的方法论甚至都是类似的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/7e0ffd77/image-20250102194609112.png&quot; alt=&quot;image-20250102194609112&quot;&gt;&lt;/p&gt;
&lt;p&gt;我想在这篇文章，去记录一下系统中涉及的那些指标，以及常见的方法论，进而分析一下这些指标的本质。&lt;/p&gt;
&lt;hr&gt;</summary>
    
    
    
    <category term="计算机系统" scheme="https://thysrael.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="直观理解" scheme="https://thysrael.github.io/tags/%E7%9B%B4%E8%A7%82%E7%90%86%E8%A7%A3/"/>
    
    <category term="S10假期" scheme="https://thysrael.github.io/tags/S10%E5%81%87%E6%9C%9F/"/>
    
    <category term="计算机系统" scheme="https://thysrael.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-流浪厦门</title>
    <link href="https://thysrael.github.io/posts/d3c9a793/"/>
    <id>https://thysrael.github.io/posts/d3c9a793/</id>
    <published>2025-06-13T01:55:10.000Z</published>
    <updated>2025-08-15T12:16:48.016Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这么多人我不知道还算不算流浪，</p><p>可是从沙茶汤上刮起的风只掠过了我。</p><p>我急匆匆走过地图上标记的每一个浪漫角落，</p><p>自己却永远走不出地图。</p><p>我还记得凤凰花别在你耳朵上的样子。</p><p>所以现在你的波希米亚长裙去了哪里？</p></blockquote><h2 id="一、感受"><a href="#一、感受" class="headerlink" title="一、感受"></a>一、感受</h2><p>这次出游是实验室组织的团体活动，我有幸担任了旅游的组长。但是因为组长的缘故，在景点和美食的选择上并不能随心所欲，而是更要考虑大家的需求。不过这也并不是什么坏事情，如果由着我的性子，我一定会在宾馆躺三天。</p><p>厦门文旅给人的最直观印象就是“浪漫”。宣传里的十里长堤、海上列车、曾厝垵和鼓浪屿，是这座城市的浪漫名片。不过我倒没有明显感觉出很浪漫的地方，这里的浪漫似乎因为旅游开发的缘故，显得有些模式化。更感慨的是，沙坡尾，明明在小红书中被形容成“漫画感”，在我看来，十分萧条落寞：那些本应鲜艳的油漆，在离开了镜头后，显得斑驳和衰老。</p><p>但是那又如何呢？我相信它是浪漫的，不是因为那些景点，而是因为它在我的脑海里本来就是浪漫的。我相信那里每个姑娘都穿着各式各样的波西米亚长裙，海风会将裙摆轻轻吹起。凤凰花会像火红的瀑布一样，溢满整个巷子。</p><p>如果抛去浪漫这个元素，厦门是非常惊喜的，无论是历史文化还是自然景色，都非常好；当然吃得也非常好，除了姜母鸭！</p><hr><h2 id="二、地理"><a href="#二、地理" class="headerlink" title="二、地理"></a>二、地理</h2><p>厦门是福建省的一个城市，它直面大海，岛屿众多。</p><p><img src="/posts/d3c9a793/bba1cd11728b471089cf4434c8cec3fdfd032361.jpeg" alt="img"></p><p>而且向东远眺，就可以看到台湾省的金门岛。</p><p><img src="/posts/d3c9a793/730e0cf3d7ca7bcba1857ed7b5096b63f724a848.jpeg" alt="img"></p><p>厦门市是有思明岛和周围的半岛组成的，主要的景点都集中于思明岛：</p><p><img src="/posts/d3c9a793/d3f3f0de-2174-413e-9dcc-8619089db4f0.png" alt="d3f3f0de-2174-413e-9dcc-8619089db4f0"></p><p>更具体而言，思明岛上有 3 个景点群，集美学村有 1 个景点群：</p><p><img src="/posts/d3c9a793/c72fc5ed-e882-4e01-9911-1ddca03518e7.png" alt="c72fc5ed-e882-4e01-9911-1ddca03518e7"></p><p>鼓浪屿单独有一个景点地图（景点实在是太密集了）：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_154700_649.png" alt="微信图片_2025-06-14_154700_649"></p><p>厦门市内的共享单车和电动车并不是很多，地铁好像也不是太发达（当然可能也跟我没有深入探索有关系），我们多采用出租的形式，不过一个景点群里的景点，也可以采用步行的方式。</p><p>最后放一张厦门地图：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_145753_635.png" alt="微信图片_2025-06-14_145753_635"></p><hr><h2 id="三、景点"><a href="#三、景点" class="headerlink" title="三、景点"></a>三、景点</h2><h3 id="3-1-沙坡尾"><a href="#3-1-沙坡尾" class="headerlink" title="3.1 沙坡尾"></a>3.1 沙坡尾</h3><p>我们就住在了沙坡尾边儿上，所以到了以后的第一天我们就打算去沙坡尾看看。我非常期待这个地方，因为它在攻略里是长成这个样子的：</p><p><img src="/posts/d3c9a793/820_Chxky2PjuAeARinkAAco2Kvvsrg340.jpg" alt="img"></p><p>只可惜我们当天去的时候，攻略中的焦糖玛奇朵滤镜直接换成 BBC 阴间滤镜了：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_200725_520.png" alt="微信图片_2025-06-13_200725_520"></p><p>路上淅淅沥沥几家商户，卖的还都是景区套餐，我觉得就算是在模式化商业街里面，它也是排在南锣鼓巷后面的主儿。</p><p>我们去沙坡尾的时候会路过小区，这里的小区有一种“城中村”的感觉，与周星驰《功夫》里的猪笼城寨有点类似，但是要更加具有生活气息。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_201113_459.png" alt="微信图片_2025-06-13_201113_459"></p><p>不过我真的理解不了他们为什么不整整齐齐的建楼房，平原人的强迫症神烦。</p><h3 id="3-2-鼓浪屿"><a href="#3-2-鼓浪屿" class="headerlink" title="3.2 鼓浪屿"></a>3.2 鼓浪屿</h3><p>鼓浪屿是思明岛旁的一个小岛，在鸦片战争后，各国相继在岛上设置领事馆，所以这是一个充满异域风情的小岛，也是厦门文旅最耀眼的名片。</p><p>从思明岛去鼓浪屿要坐船，船票有 35 的也有 80 的，似乎没有什么区别，早到一些可以上二楼，有座位，而且景色也更好：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_210709_087.png" alt="微信图片_2025-06-13_210709_087"></p><p>在船上可以看到鼓浪屿上立着的郑成功雕像，据说面向的方向就是台湾的方向：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_211044_458.png" alt="微信图片_2025-06-13_211044_458"></p><p>一进鼓浪屿就是网红打卡点 —— 最美转角，我其实很不理解这种打卡点存在的意义，没有逻辑啊：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_212047_025.png" alt="微信图片_2025-06-13_212047_025"></p><p>然后按照推荐路线我们应该向这个转角的左侧走去，而我们毅然决然地选择了右侧，主要原因是我觉得右侧有更多漂亮的穿裙子姑娘。我们的下一站是月光岩，这是鼓浪屿上的第二高峰，我们之所以去这里，也很简单，因为第一高峰日光岩要 50 块钱的门票，而我们并没有这个钱。在去月光岩的路上需要走这种山路阶梯：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_212414_096.png" alt="微信图片_2025-06-13_212414_096"></p><p>其实避开“规定路线”后，鼓浪屿的景色很美，热带植物层出不穷，民宿建筑和西式建筑有一个很好的融合，而且甚至这里还有一种烟火气和旅游气的融合（明明这两者是冲突的）：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_212619_412.png" alt="微信图片_2025-06-13_212619_412"></p><p>月光岩上的风景也很好，可以看到思明岛，只可惜月光岩在正中间，不如日光岩，可以看到金门岛。但是它不要钱，所以它就是最好的（除了笔架山很难找到入口外）：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213032_905.png" alt="微信图片_2025-06-13_213032_905"></p><p>下山的路上开满了凤凰花，就像燃烧的瀑布一样：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213300_040-1749821594496-23.png" alt="微信图片_2025-06-13_213300_040"></p><p>有些凤凰花落到了地上，就像瀑布溅出来的水洼：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213352_209.png" alt="微信图片_2025-06-13_213352_209"></p><p>在下山路的尽头是第二转角，我没有感觉出它和第一转角的区别，也没有感觉出它和任意一个楼角的区别：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213714_097.png" alt="微信图片_2025-06-13_213714_097"></p><p>如果往人流更多的地方去走，就会遇到一些经典商业街，感觉要比普通的商业街要更加“商业”一些：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213810_387.png" alt="微信图片_2025-06-13_213810_387"></p><p>另一些商业街：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_213813_387.png" alt="微信图片_2025-06-13_213813_387"></p><p>这里的榕树都好大啊：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_214203_986.png" alt="微信图片_2025-06-13_214203_986"></p><p>沙滩也看过了，但是天气不怎么好，此外似乎沙子也没有那么细软（但是踩上去也很舒服了）：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_214553_039.png" alt="微信图片_2025-06-13_214553_039"></p><p>在沙滩上发现了一个水母，人生或许就跟这个一样易逝，或者说被海浪卷上岸后，离彻底蒸发干净还需要过去很久：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_214617_606.png" alt="微信图片_2025-06-13_214617_606"></p><p>我们还尝试了一下踩着礁石往海里面走去，哈哈哈哈我们太怂了：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_145344_974.png" alt="微信图片_2025-06-14_145344_974"></p><p>另外就是鼓浪屿上的小姐姐好多都穿着裙子啊。</p><h3 id="3-3-八市-amp-中山路"><a href="#3-3-八市-amp-中山路" class="headerlink" title="3.3 八市 &amp; 中山路"></a>3.3 八市 &amp; 中山路</h3><p>我们是离开鼓浪屿以后去的八市和中山路（两个地方离得挺近，可以步行）。因为我打错车的缘故，所以八市我没有进去，直接到了中山路。不过据去的同学说，八市确实只是一个菜市场，并不是一个商业街，所以可能买生食材的比较多，而饭店比较少。最后我们是在八市的街尾汇合的，可以看到这里还是没有商业街的那种浮华的：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_143511_415.png" alt="微信图片_2025-06-14_143511_415"></p><p>而中山路就是正经商业街的，街道两旁的油漆都很新：</p><p><img src="/posts/d3c9a793/微信图片_20250614140935.jpg" alt="微信图片_20250614140935"></p><p>这两侧的楼都被叫作骑楼，也就是第一层是街面，而第二层及以上都是完全盖住第一层街面的设计，这样的目的主要是为了避雨。也不知道是幸运还是不幸运，我们去中山路的时候大雨倾盆，我就在骑楼下来回穿梭，结结实实体验了一下这个建筑的妙用。</p><p><img src="/posts/d3c9a793/微信图片_20250614140950.jpg" alt="微信图片_20250614140950"></p><p>这里的商铺的招牌都还挺有改开时的年代感，可以看出厦门作为桥头堡的历史痕迹：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_143855_882.png" alt="微信图片_2025-06-14_143855_882"></p><h3 id="3-4-索道"><a href="#3-4-索道" class="headerlink" title="3.4 索道"></a>3.4 索道</h3><p>我在找攻略的时候就看上这个索道了，能够在索道上俯瞰厦门，感觉非常酷。因为我和我的朋友实在是太沉了，没法共用一个车厢，所以因祸得福获得了独立车厢：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_155001_508.png" alt="微信图片_2025-06-14_155001_508"></p><p>但是我是真没想到，它居然这么高，孩子恐高啊，而且我们去的那天，又是刮风，又是下雨的，那个车厢就晃晃悠，晃晃悠；雨点嘀嘀嗒，嘀嘀嗒，吓都把我吓死了：</p><p><img src="/posts/d3c9a793/微信图片_20250614150423.jpg" alt="微信图片_20250614150423"></p><p>不过上面的景色还是很好的，可以看到很多热带特色植物：</p><p><img src="/posts/d3c9a793/微信图片_20250614150323.jpg" alt="微信图片_20250614150323"></p><p>还可以看到一些在平原城市中难以看到的景色：</p><p><img src="/posts/d3c9a793/微信图片_20250614150336.jpg" alt="微信图片_20250614150336"></p><p>还可以远眺双子塔（上塔居然要 250 多块钱，好贵啊）：</p><p><img src="/posts/d3c9a793/微信图片_20250614150353.jpg" alt="微信图片_20250614150353"></p><h3 id="3-5-集美学村"><a href="#3-5-集美学村" class="headerlink" title="3.5 集美学村"></a>3.5 集美学村</h3><p>集美学村在思明岛外，要到集美学村可以做网红的“海上列车” —— 地铁一号线。而实际效果，就是普通的轻轨，并没有乘风破浪的感觉，因为从窗户向外望去，大部分的都是其他轨道，能看到的海面，也并不是很壮阔：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_161539_159.png" alt="微信图片_2025-06-14_161539_159"></p><p>下了地铁就是龙舟池，临近端午，有人在练习龙舟：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_161857_430.png" alt="微信图片_2025-06-14_161857_430"></p><p>再往前走就是集美中学，是爱国华侨陈嘉庚先生建立的学校，建筑非常有特点：</p><p><img src="/posts/d3c9a793/微信图片_20250614160601.jpg" alt="微信图片_20250614160601"></p><p>不过一想到上这个学校的人居然还要高考，我就想笑：</p><p><img src="/posts/d3c9a793/微信图片_20250614160617.jpg" alt="微信图片_20250614160617"></p><p>在集美中学后就是陈嘉庚先生为爱国华侨修建的归来堂：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_162246_830.png" alt="微信图片_2025-06-14_162246_830"></p><p>离开了归来堂就是陈嘉庚先生故居：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_162250_107.png" alt="微信图片_2025-06-14_162250_107"></p><p>故居里有一个沙发，据说是为了方便陈嘉庚先生办公，加装了桌板：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_162253_000.png" alt="微信图片_2025-06-14_162253_000"></p><p>最后我们还去参观了大社，不知道为什么，总感觉有种民俗朋克的感觉：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_165114_053.png" alt="微信图片_2025-06-14_165114_053"></p><h3 id="3-6-十里长堤"><a href="#3-6-十里长堤" class="headerlink" title="3.6 十里长堤"></a>3.6 十里长堤</h3><p>我之前一直以为十里长堤是一个“自古以来”存在的景点，没有想到居然是 2011 年左右才炒起来的景点，所以直接看上去，居然有点像石家庄音乐节：</p><p><img src="/posts/d3c9a793/微信图片_20250614165122.jpg" alt="微信图片_20250614165122"></p><p>十里长堤本身似乎也平平无奇：</p><p><img src="/posts/d3c9a793/微信图片_20250614165039.jpg" alt="微信图片_20250614165039"></p><p>海上列车如果从岸边的角度去看，似乎好看了一些？</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_165459_505.png" alt="微信图片_2025-06-14_165459_505"></p><p>然而就在我们离开十里长堤，就成功错过了最美晚霞，我是真急了：</p><p><img src="/posts/d3c9a793/微信图片_20250614165130.jpg" alt="微信图片_20250614165130"></p><p>在飞机上看似乎也不错：</p><p><img src="/posts/d3c9a793/微信图片_20250614165143.jpg" alt="微信图片_20250614165143"></p><hr><h2 id="四、美食"><a href="#四、美食" class="headerlink" title="四、美食"></a>四、美食</h2><h3 id="4-1-鹦哥楼"><a href="#4-1-鹦哥楼" class="headerlink" title="4.1 鹦哥楼"></a>4.1 鹦哥楼</h3><p>鹦哥楼是我在沙坡尾找到的一个非常经济实惠的饭店，本身是一个历史建筑物，因为顶楼上有一个鹦鹉雕塑，所以被叫作鹦哥楼。</p><p>但是遗憾的是，里面的饭菜真的很难吃。能吃得下的都是一些哪里都有的菜，比如说鲍汁山菌、虾球、茶香脆骨。但是一旦涉及了一些本地特色，似乎都带上了一种“寡淡诡异”的感觉：</p><ul><li>蒜香手工豆腐：豆腐有些发酵带来的酸口，但是我觉得为了一点特殊的滋味，牺牲了豆腐本身的口感（软的近似液体了）不值当。</li><li>黑蒜文蛤炖中排：非常清单，只有肉本身的腥味，而黑蒜有让食物带上了一种腻糊糊的感觉。</li><li>五花肉夹馍：五花肉非常肥，确实一咬就可以香得流油儿，但是有些过于腻，甚至都腻得发甜了。</li><li>酸菜筒骨：非常腥，而且酸菜非常难吃，我没有吃过这么难吃的酸菜。</li><li>醋肉：本来是冲着这个名字去点的，以为是那种有清爽醋香，能开开胃，但是实际上更像是酵酸味儿，而且肉汁也并不紧实。</li><li>沙茶烩：这个汤里面的每一个东西，还没有清汤锅里煮出来的咸。</li></ul><p>当然这几个难吃就难吃了，好歹还是特色的，有两样菜非常气人。一个是姜母鸭，这个是厦门的名菜，但是实际上非常难吃，鸭子很大，所以肉质很柴，而且并不入味，我感觉我像是在吃一只白水煮鸭子，而且这只鸭子本身也不嫩，肉是肉，油是油的。</p><p>另外一个就是底下这碗红菇猪肉汤了，卖整整 36 块大洋，跟涮锅水没有任何区别，红菇没有常见菌类的香气，猪肉真的就是白水煮猪肉。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_183331_809.png" alt="微信图片_2025-06-13_183331_809"></p><p>结合后面几次吃到的饭，我觉得可能也不止是这家店的问题，我觉得可能整个厦门在对于“大肉”的处理上都没有办法契合我一个北方人的口味，在来之前，我实在是想象不到有人可以把排骨、五花肉、里脊这种怎么做都好吃的肉做得这么诡异，也想象不到居然一整只鸭子能做成没有一个地方是好吃的，鸭脖子、鸭腿、鸭内脏、鸭掌、鸭胸这些哪哪都不一样的肉是怎么给做成一样的难吃的啊？不过反过来说，只要不涉及大肉，福建的菜还都挺好吃的。</p><p>不过这里的服务很好，而且感觉很有闽南特色。</p><h3 id="4-2-鲨鱼丸"><a href="#4-2-鲨鱼丸" class="headerlink" title="4.2 鲨鱼丸"></a>4.2 鲨鱼丸</h3><p>我们在沙坡尾附近的一家店吃吃到的，按照店家的说法是纯手动制作的鲨鱼丸，我一个北方人对于鱼丸真的没有抵抗力。而且真的很好吃。鲨鱼丸的肉要比普通的鱼丸更加紧实弹牙，而且肉的颗粒感会更加明显。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_193806_314.png" alt="微信图片_2025-06-13_193806_314"></p><p>而且里面还是有馅儿的，甚至馅儿的口感也很有层次，而不是那种超市的预制撒尿牛丸里的软趴趴的馅料可以相比的：</p><p><img src="/posts/d3c9a793/微信图片_20250613192410.jpg" alt="微信图片_20250613192410"></p><p>除了丸子本身好吃以外，这个看似平平无奇的汤也非常好喝，鲜到它能直接顺着嗓子眼儿滑下去，舌头怎么搂都搂不住。</p><p>唯一的缺点就是稍微有点贵，我忘记是 25 还是 30 了。</p><h3 id="4-3-火参果"><a href="#4-3-火参果" class="headerlink" title="4.3 火参果"></a>4.3 火参果</h3><p>在鼓浪屿上看到的，一个 10 块钱就买了，老板娘还特意帮我挑了一个红的：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_211116_385.png" alt="微信图片_2025-06-13_211116_385"></p><p>味道也没有很独特，类似于百香果，没有特别酸，但是也没有特别甜，而且汁水也不是很多（嘬吸管累死我了）：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-13_211736_888.png" alt="微信图片_2025-06-13_211736_888"></p><h3 id="4-4-上屿水产"><a href="#4-4-上屿水产" class="headerlink" title="4.4 上屿水产"></a>4.4 上屿水产</h3><p>上屿水产是我们在鼓浪屿上找的一家海鲜店，经济实惠（均 150）的同时服务也很好。一扫我对于闽南菜很难吃的担心。</p><p>蛰头没有什么稀奇的，我看重的是这个是用永春醋来泡的。永春醋是闽南的特色醋，所以我很想尝一尝是什么味道。颜色方面是比普通的醋要棕一些，口感上要更加醇厚，而味道上不怎么香，也不怎么甜。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_111110_858.png" alt="微信图片_2025-06-14_111110_858"></p><p>豉油蒸乌耳鳗是这里面最好吃的一道菜，这是刚上桌的样子，等过了一轮再转回来的时候，连一瓣鱼肉都没有了。我觉得相比于日料店里的鳗鱼，它没有过于紧实；而相比于黄花鱼这种一筷子刀下去，肉彻底散了的鱼，有可以将汤汁紧紧锁在鱼肉里面。</p><p>再加上这里的鱼皮也没有像日料中的鳗鱼一样和鱼肉泾渭分明，以至于单独吃过腻，合在一起吃会在口腔里解体。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_111354_675.png" alt="微信图片_2025-06-14_111354_675"></p><p>皮皮虾也很常见，反正就只是不难吃，没有那种非常应时的皮皮虾的甜美。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_112217_912.png" alt="微信图片_2025-06-14_112217_912"></p><p>五香卷是闽南的特色美食，它是用油豆皮裹上肉、荸荠、洋葱，然后过油炸。就这个做法光听起来，就知道肯定难吃不了！</p><p>事实也确实如此，这个非常好吃。但是吃到后面，我总感觉有一种在吃“羊肠衣”而不是“油豆皮”的感觉，应该是因为里面掺了过量的肥肉，导致吃到最后有些腻人和腥气了（甚至有种吃羊肉串的感觉），不过也没有那么夸张，吃起来还是很好吃的。我在后面几次吃饭的时候，也点了这个菜。</p><p>右边的那个酱并不是普通的甜辣酱，而是这里特色的一种甜辣酱，相比于普通的甜辣酱，要更加的酸，而且有酵香（为什么这里什么东西都有酵香）。辣度也会弱一些，本来我是很喜欢吃不怎么辣的甜辣酱的，但是这个酱我是真的吃不惯，吃多了就有一种在干嚼呕吐物的感觉。</p><p><img src="/posts/d3c9a793/微信图片_20250614110924.jpg" alt="微信图片_20250614110924"></p><p>同安封肉，是蒸肉的一种，我觉得并不好吃，一个原因是因为这个五花肉肥的太多，瘦的太少。另一个原因是这个肥肉的口感非常奇怪，没有一点油脂的香气，更像是冬瓜和果粒爽里面的果粒的混合体，没有肉味也没有肉的口感。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_113518_582.png" alt="微信图片_2025-06-14_113518_582"></p><p>小青龙，非常普通，不过我也没吃过啥好的龙虾，反正我觉得味道和普通的虾差不多。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_114005_655.png" alt="微信图片_2025-06-14_114005_655"></p><p>干煎膏蟹，超级有特色且好吃的一道菜。我们平时吃的螃蟹都是蒸的，所以肉质会更加润滑松弛一些。而这道菜恰恰相反，采用干煎的手法，让蟹肉和蟹黄收缩到一起，提供新奇口感的同时紧紧锁住了蟹香。</p><p>而且不知道是怎么处理的，这个蟹的蟹肉吃完后口腔内会有一种干辣的涩感，让人食欲大开。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_114247_985.png" alt="微信图片_2025-06-14_114247_985"></p><p>最后还能说什么呢？干杯！</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_114757_707.png" alt="微信图片_2025-06-14_114757_707"></p><h3 id="4-5-厚生林"><a href="#4-5-厚生林" class="headerlink" title="4.5 厚生林"></a>4.5 厚生林</h3><p>上屿水产一出来就是厚生林，是一个买类似于冰镇甜粥的饮品店。</p><p>我最喜欢吃里面的蜜藕了，枣红色的蜜藕，吃起来虽然不脆，但是紧实绵密（我似乎用了好多次“紧实”这个词了），咬下去以后不会被腌渍过的藕心儿腻住，反而是蜜味儿随着藕的纤维组织一点点在口腔里润开，加上藕是拿冰镇过的，所以完全不粘牙，实在是难得。</p><p><img src="/posts/d3c9a793/微信图片_20250614140808.jpg" alt="微信图片_20250614140808"></p><p>其他的配料，比如说银耳、莲子、百合，也非常好吃，这些配料和北方的八宝粥里面的类似，但是八宝粥是热的，而且是用大米来做基底，而这里是用糖水做基底，然后再冰镇。所以八宝粥喝起来黏黏糊糊，多种配料都混合交织在了一起，而这里的甜粥，则是用冰隔离了不同配料的口感，甚至锁住了不同配料的口感。</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_142348_079.png" alt="微信图片_2025-06-14_142348_079"></p><h3 id="4-6-夏氏沙茶面"><a href="#4-6-夏氏沙茶面" class="headerlink" title="4.6 夏氏沙茶面"></a>4.6 夏氏沙茶面</h3><p>这是我们在中山路上找到的一家店，完全没有预谋，看店门口说是上过电视，所以就进来尝一尝。</p><p>这家的老板真的非常 nice，我们因为经费的原因，六个人只点了两碗沙茶面，结果人家老板愣是拿了六个碗乘这两碗沙茶面，每碗都跟下面这碗一样满满当当的卤和面。</p><p><img src="/posts/d3c9a793/微信图片_20250614140906.jpg" alt="微信图片_20250614140906"></p><p>这家做得沙茶面是真的非常好吃，沙茶因为里面含有虾米的缘故，所有很喇嗓子而不够醇厚，而麻酱呢，虽然醇厚但是糊嗓子。这里调的卤子，不但没有传统沙茶酱的尖涩感，而且还有一种不同于麻酱的厚重，也就是并不黏糊的同时，还有一些重量感，温吞的整体口感里包裹着一些海鲜的刺激。</p><p>蛤仔煎，也是特色没事，应该是把蛤蜊、韭菜和鸡蛋摊在一起。蛤蜊的口感被韭菜和鸡蛋中和了很多，没有很明显的腥味和涩味。但是相应的，蛤蜊的咸鲜味也弱了一些，只剩下蛤蜊的口感被完整保留下来，甚至和韭菜鸡蛋形成了新的范式。不过他们又在上面洒上厦门特有甜辣酱了（甚至有一整桶甜辣酱摆在桌子上）。</p><p><img src="/posts/d3c9a793/微信图片_20250614140843.jpg" alt="微信图片_20250614140843"></p><p>蟹黄汤包就是烂大街的那种，在上海呆久了，我已经对这种东西去魅了：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_160956_905.png" alt="微信图片_2025-06-14_160956_905"></p><p>店家还赠送了五香卷，味道和在上屿水产吃到的类似：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_161311_036.png" alt="微信图片_2025-06-14_161311_036"></p><p>吃完以后我又在街边买了四果汤，别看名字取得很好听，但是完全不如冰粥好喝，都是预制品和添加剂。</p><p>不过我查了查四果汤指的是“红豆、绿豆、莲子、薏仁”，我当时喝到的并没有这些东西，可能并不是四果汤不好喝，而是这家店偷工减料坑人。</p><p><img src="/posts/d3c9a793/微信图片_20250614140922.jpg" alt="微信图片_20250614140922"></p><h3 id="4-7-此食此茶"><a href="#4-7-此食此茶" class="headerlink" title="4.7 此食此茶"></a>4.7 此食此茶</h3><p>这趟旅行最好的一家店，不但经济实惠，而且吃得最偏向于北方口味，而且不失闽南特色。</p><p><img src="/posts/d3c9a793/微信图片_20250614162747.jpg" alt="微信图片_20250614162747"></p><p>最关键是环境非常好，可以看到那种在宋词里面才会出现的廊庭：</p><p><img src="/posts/d3c9a793/微信图片_20250614162758.jpg" alt="微信图片_20250614162758"></p><p>二楼还有天台，虽然只能看见对面的幼儿园：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_163345_552.png" alt="微信图片_2025-06-14_163345_552"></p><p>鱼豆腐非常弹牙，而且有一种炸物一般没有的新鲜（怎么又有甜辣酱呀）：</p><p><img src="/posts/d3c9a793/微信图片_20250614162820.jpg" alt="微信图片_20250614162820"></p><p>羊肚菌酿虾滑是我吃过最好吃的羊肚菌，没有菌干那种突出的土腥味儿：</p><p><img src="/posts/d3c9a793/微信图片_2025-06-14_163811_624.png" alt="微信图片_2025-06-14_163811_624"></p><p>厦门特色咸饭，反正我觉得比上海菜泡饭好吃：</p><p><img src="/posts/d3c9a793/微信图片_20250614162945.jpg" alt="微信图片_20250614162945"></p><p>其他的饭也都很好吃，就来不及拍照了。不过我又不死心点了姜母鸭，发现即使在这个非常好吃的饭店里，姜母鸭依然很难吃。鸭子外面有姜的辛辣，而鸭肉依然不入味儿。我在鼓浪屿上看到了姜母鸭的做法，说的是用老姜（也就是“姜母”）炖整只鸭子，整只鸭子可是一刀不剁啊，怪不得外面辣得要死，里面不入味儿呢。</p><hr><h2 id="五、歌单"><a href="#五、歌单" class="headerlink" title="五、歌单"></a>五、歌单</h2><ul><li>Summer (久石让)</li><li>把握时间，掌握方向（林生祥）</li><li><strong>望春风</strong>（刘惜君）</li><li>我们的时光（赵雷）</li><li><strong>亚洲挚爱</strong>（红节奏）</li><li>城市的浪漫运作（甜约翰）</li><li>Grapejuice（Harry Styles）</li><li>面会菜（林生祥）</li><li>かざぐるま（山崎ハコ）</li></ul>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这么多人我不知道还算不算流浪，&lt;/p&gt;
&lt;p&gt;可是从沙茶汤上刮起的风只掠过了我。&lt;/p&gt;
&lt;p&gt;我急匆匆走过地图上标记的每一个浪漫角落，&lt;/p&gt;
&lt;p&gt;自己却永远走不出地图。&lt;/p&gt;
&lt;p&gt;我还记得凤凰花别在你耳朵上的样子。&lt;/p&gt;
&lt;p&gt;所以现在你的波希米亚长裙去了哪里？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;一、感受&quot;&gt;&lt;a href=&quot;#一、感受&quot; class=&quot;headerlink&quot; title=&quot;一、感受&quot;&gt;&lt;/a&gt;一、感受&lt;/h2&gt;&lt;p&gt;这次出游是实验室组织的团体活动，我有幸担任了旅游的组长。但是因为组长的缘故，在景点和美食的选择上并不能随心所欲，而是更要考虑大家的需求。不过这也并不是什么坏事情，如果由着我的性子，我一定会在宾馆躺三天。&lt;/p&gt;
&lt;p&gt;厦门文旅给人的最直观印象就是“浪漫”。宣传里的十里长堤、海上列车、曾厝垵和鼓浪屿，是这座城市的浪漫名片。不过我倒没有明显感觉出很浪漫的地方，这里的浪漫似乎因为旅游开发的缘故，显得有些模式化。更感慨的是，沙坡尾，明明在小红书中被形容成“漫画感”，在我看来，十分萧条落寞：那些本应鲜艳的油漆，在离开了镜头后，显得斑驳和衰老。&lt;/p&gt;
&lt;p&gt;但是那又如何呢？我相信它是浪漫的，不是因为那些景点，而是因为它在我的脑海里本来就是浪漫的。我相信那里每个姑娘都穿着各式各样的波西米亚长裙，海风会将裙摆轻轻吹起。凤凰花会像火红的瀑布一样，溢满整个巷子。&lt;/p&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    <category term="流浪厦门" scheme="https://thysrael.github.io/tags/%E6%B5%81%E6%B5%AA%E5%8E%A6%E9%97%A8/"/>
    
    <category term="S10课上" scheme="https://thysrael.github.io/tags/S10%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
  <entry>
    <title>海边拾贝-FlashInfer</title>
    <link href="https://thysrael.github.io/posts/7df595f9/"/>
    <id>https://thysrael.github.io/posts/7df595f9/</id>
    <published>2025-05-06T13:15:38.000Z</published>
    <updated>2025-08-15T12:16:48.895Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>Attention Engine 可以被理解成<strong>“Attetion 算子库 + Attention 运行时</strong>”。有以下设计：</p><ul><li>可拆分的 Attention 算子：提高了 GPU 内存带宽利用率</li><li>新的 KV Cache 管理抽象 CBSR：兼具 PagedAttention 和 RadixAttention 的优点，更 general</li><li>宏观上动态、微观上静态的调度运行时：动态的同时不损害静态抽象和收益</li><li>可定制的 Attention 算子框架、JIT：一些工程特性</li></ul><p>FlashInfer 这篇工作在 2023 年就提出了，据作者所言，那个时候只有 FlashAttention1，还没有 FA2&amp;3，FlashDecode 等工作，但是这些工作论文发得更早。</p></blockquote><h2 id="一、Background"><a href="#一、Background" class="headerlink" title="一、Background"></a>一、Background</h2><h3 id="FlashAttention"><a href="#FlashAttention" class="headerlink" title="FlashAttention"></a>FlashAttention</h3><p>传统的 Attention 运算需要扫描 3 遍 GPU Memory 中的 Attention Logits 矩阵（len, len），计算密度低。</p><p>FlashAttention 提出了 1-pass 的 attention 算法，提高了计算密度，缓解了 GPU Memory 到 GPU Cache 的 IO 瓶颈：</p><p><img src="/posts/7df595f9/1746537571691-4.gif" alt="img"></p><h3 id="FlashAttention1-不适应-Decode-场景"><a href="#FlashAttention1-不适应-Decode-场景" class="headerlink" title="FlashAttention1 不适应 Decode 场景"></a>FlashAttention1 不适应 Decode 场景</h3><p>FlashAttention 是为了训练开发的，Queries 的长度往往很长（也就是 Queries 彩色方块的高度很高），这样可以充分利用 GPU 上的计算资源。</p><p>而在 decode 阶段，采用自回归生成，每次的 Queries 的长度就为 1（也就是 Queries 彩色方块高度只有 1），那么计算资源就不会得到充分利用（按照 FlashDecode 的说法，是不到 1%）。</p><p>更加形式化地去看，在 FlashAttention 中，设置 <script type="math/tex">l_q</script>是 Queries 的长度，<script type="math/tex">l_k</script>是 K 的长度，那么一次 Attention 计算的访存开销是 <script type="math/tex">O(l_q + l_k)</script>，计算开销是 <script type="math/tex">O(l_q l_k )</script>，则计算密度是：</p><script type="math/tex; mode=display">O(\frac{l_q l_k}{l_q + l_k}) = O(\frac{1}{\frac{1}{l_q} + \frac{1}{l_k}} )</script><p>如果考虑 <script type="math/tex">l_{k}</script>是一个很大的值（长文本或者推理模型都会导致 <script type="math/tex">l_k</script> 很大），那么计算密度约等于 <script type="math/tex">O(l_q)</script> 。当 <script type="math/tex">l_q = 1</script> 时就会导致计算资源利用不足。</p><h3 id="Attention-的输入是动态变化的"><a href="#Attention-的输入是动态变化的" class="headerlink" title="Attention 的输入是动态变化的"></a>Attention 的输入是动态变化的</h3><p>而实际情况会更加复杂，query 的长度是会动态变化的，从应用场景区分，有 3 种：</p><p><img src="/posts/7df595f9/1746537571691-1.png" alt="img"></p><p>放到 roofline 上来看</p><p><img src="/posts/7df595f9/1746537571691-2.png" alt="img"></p><p>动态变化的 Queries 长度就对 Attention 的动态性提出了一定的要求。</p><p>此外优化长文本有一种经典的技术就是 KV Cache 稀疏，也就是 KV Cache 也会存在变化（不止是单调递增），如 NSA 就包含三种稀疏特性：</p><p><img src="/posts/7df595f9/1746537571691-3.png" alt="img"></p><p>Attention 算子库既要利用特化的优势，又要有足够好的定制性。</p><hr><h2 id="二、Design"><a href="#二、Design" class="headerlink" title="二、Design"></a>二、Design</h2><h3 id="2-1-Split-K"><a href="#2-1-Split-K" class="headerlink" title="2.1 Split-K"></a>2.1 Split-K</h3><p>原版的 FlashAttention 需要利用前缀和不断缩放校正（scale）局部结果，但是“前缀和”就意味着“顺序遍历”，而当 Queries 的长度较小时，就容易导致利用率不高。</p><p>通过调整算法，我们可以实现并行计算不同的 KV Cache Chunk：</p><p><img src="/posts/7df595f9/1746537641901-13.gif" alt="img"></p><p>所以为了达到这种 merge 的效果，我们需要记录每个 block 的一些运算结果，这些结果在文中被称为 Attention State。</p><p>每个 block <script type="math/tex">\mathcal{I}</script> 需要记录两种 State，分别是 attention scale：</p><p><img src="/posts/7df595f9/1746537641901-14.png" alt="img"></p><p>和 attention output：</p><p><img src="/posts/7df595f9/1746537641901-15.png" alt="img"></p><p>有了这两个东西以后，我们就可以将 block <script type="math/tex">\mathcal{I}</script> 和 block <script type="math/tex">\mathcal{J}</script> 融合到一起了：</p><p><img src="/posts/7df595f9/1746537641902-16.png" alt="img"></p><p>Split-K 算法改善了 FA1 在 Decode 场景下计算资源利用率不足的问题。</p><p>Split-K 策略也是 FlashDecode 的核心 Idea，对此 FI 的作者叶子豪解释道：</p><blockquote><p>其实主要原因是我们跟 FA2 和 FlashDecoding 的开发几乎都是同期进行的，在 FlashAttention2 发布之前我们已经独立探索过了 FA2 中大部分的优化。而 FlashDecoding 我们在去年 8 月就已经有了较完整的实现和评测，不过我对 LLM 这个领域的内卷程度稍有低估没有及时推广，导致被抢发出来。抛开这些虚名而言，LLM Serving 还有很多工程上的问题需要解决，同志仍需努力。</p></blockquote><h3 id="2-2-Composable-Attention"><a href="#2-2-Composable-Attention" class="headerlink" title="2.2 Composable Attention"></a>2.2 Composable Attention</h3><p>在有了 Split-K 算法之后，我们就有一个可拆分、组合的 Attention 算子。也就是说，Attention 算子所需要的 Q，KV 都可以被拆分成 Chunk，然后分步或者并行计算，只要最后规约在一起就好了。这给了设计者极大的灵活性：</p><p>比如说我们可以自由组织各个计算步骤，将不同的任务分配给不同的 threadblock：</p><p><img src="/posts/7df595f9/1746537641902-17.png" alt="img"></p><p>也可以将计算的中间结果保存下来在多个 Request 之间共享。</p><h3 id="2-3-Block-Compressed-Sparse-Row"><a href="#2-3-Block-Compressed-Sparse-Row" class="headerlink" title="2.3 Block Compressed Sparse Row"></a>2.3 Block Compressed Sparse Row</h3><p>BCSR 是 FI 管理 KV Cache 的数据格式，更本质的说，FI 在<strong>用一个**</strong>稀疏矩阵<strong><strong>来模拟</strong></strong>页表**对 KV Cache 进行管理。</p><p>我们先用 OS 上的物理页面管理举例，我们有 2 个 Proc，2 个 Physical Page。其中 Proc0 只有 Page1，Proc1 有 Page0 和 Page1，那么我们就可以用一个 bool 矩阵表示这种关系：</p><p><img src="/posts/7df595f9/1746537641902-18.png" alt="img"></p><p>其中虚线的部分组成了一个 block，说明 Page1 是被 Proc0 和 Proc1 共享的。</p><p>然后我们迁移到 PI 上，将 Proc 换成 Query Chunk，将 Page 换成 KV Cache Chunk，有：</p><p><img src="/posts/7df595f9/1746537641902-19.png" alt="img"></p><p>直接用论文中的图来看：</p><p><img src="/posts/7df595f9/1746537641902-20.png" alt="img"></p><p>上图有一些省略的是，没必要用 bool 矩阵，而是可以用张量矩阵，矩阵中的每个元素都是一个形状为 <code>(layer, head, dim)</code> 的张量。</p><p>这种稀疏矩阵的表示方法，可以模拟出“内存分页”和“共享内存”的语义，这分别对应 PageAttention 和 RadixAttention 的设计。</p><p>使用稀疏矩阵这种数据结构，可以最大限度的发现里面“成块”的张量：</p><p><img src="/posts/7df595f9/1746537641902-21.png" alt="img"></p><p>这种 block 的矩阵，说明了其中的 KV Cache Chunk 会被 share，或者 Q Chunk 会被 Share，也就是会被经常使用。那么就应该把这种 KV Cache 或者 Q 放到高层次存储（寄存器或者 cache），而那些不被 share 的，放到低层次存储中。如下所示：</p><p><img src="/posts/7df595f9/1746537641902-22.png" alt="img"></p><p>此外稀疏矩阵形式，也可以很简单的描述 KV Cache 稀疏策略（没有比矩阵更加直白的描述方式了）。</p><h3 id="2-4-Dynamic-Schedule"><a href="#2-4-Dynamic-Schedule" class="headerlink" title="2.4 Dynamic Schedule"></a>2.4 Dynamic Schedule</h3><p>PI 调度的单位是 Attention 的小 block，调度算法的输入是一个 batch 内 QKV 的长度信息和当前硬件的架构信息（比如 TensorCore 的尺寸）。</p><p>调度的目标是 SM 的负载均衡，因为 Attention 可被拆分，所以调度算法设计并不难，如下所示：</p><p><img src="/posts/7df595f9/1746537641902-23.png" alt="img"></p><p>按照论文的说法，PI 采用的是一种“确定性调度”，这更有利于 LLM Serving，而且还可以发挥 CUDA Graph 的优势。</p><p>但是似乎这样就和动态调度有些违背，这里的动态调度指的是宏观上可以根据一个 batch 内的不同信息指定静态调度方针。</p><h3 id="2-5-Other"><a href="#2-5-Other" class="headerlink" title="2.5 Other"></a>2.5 Other</h3><ul><li>JIT：生成 CUDA 代码而不是 Triton 代码，一步到位</li><li>硬件架构：根据 GPU 架构选择 <code>LDGSTS</code> 还是 <code>TMA</code> ，使用 TensorCore 还是 CUDACore，Chunk Size</li><li>一系列 custom api，用于自定义 Attention 机制（FI 更像是一个 Attention 框架）。</li><li>融合 ROPE</li></ul><hr><h2 id="三、Eval"><a href="#三、Eval" class="headerlink" title="三、Eval"></a>三、Eval</h2><h3 id="3-1-端到端推理"><a href="#3-1-端到端推理" class="headerlink" title="3.1 端到端推理"></a>3.1 端到端推理</h3><p>使用 SGLang(FI) 与 SGLang(Triton) 和 TRTLLM 对比。</p><p>相比于 SGLang(Triton)，FI 的 FA 使用、JIT、动态调度使得其有良好的表现。</p><p>TRTLLM 作为 NVIDIA 的 Oracle，FI 有近似的表现。</p><p><img src="/posts/7df595f9/1746537641902-24.png" alt="img"></p><h3 id="3-2-Kernel-测试"><a href="#3-2-Kernel-测试" class="headerlink" title="3.2 Kernel 测试"></a>3.2 Kernel 测试</h3><p>与 FA2&amp;3 进行对比，主要测试的是针对 Query 的动态特性，能不能有及时的反应：</p><p><img src="/posts/7df595f9/1746537641902-25.png" alt="img"></p><p>可以看到当 Query 具有动态性的时候，FI 更胜一筹，这说明 FI 的 runtime 更强。</p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;Attention Engine 可以被理解成&lt;strong&gt;“Attetion 算子库 + Attention 运行时&lt;/strong&gt;”。有以下设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可拆分的 Attention 算子：提高了 GPU 内存带宽利用率&lt;/li&gt;
&lt;li&gt;新的 KV Cache 管理抽象 CBSR：兼具 PagedAttention 和 RadixAttention 的优点，更 general&lt;/li&gt;
&lt;li&gt;宏观上动态、微观上静态的调度运行时：动态的同时不损害静态抽象和收益&lt;/li&gt;
&lt;li&gt;可定制的 Attention 算子框架、JIT：一些工程特性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FlashInfer 这篇工作在 2023 年就提出了，据作者所言，那个时候只有 FlashAttention1，还没有 FA2&amp;amp;3，FlashDecode 等工作，但是这些工作论文发得更早。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;一、Background&quot;&gt;&lt;a href=&quot;#一、Background&quot; class=&quot;headerlink&quot; title=&quot;一、Background&quot;&gt;&lt;/a&gt;一、Background&lt;/h2&gt;&lt;h3 id=&quot;FlashAttention&quot;&gt;&lt;a href=&quot;#FlashAttention&quot; class=&quot;headerlink&quot; title=&quot;FlashAttention&quot;&gt;&lt;/a&gt;FlashAttention&lt;/h3&gt;&lt;p&gt;传统的 Attention 运算需要扫描 3 遍 GPU Memory 中的 Attention Logits 矩阵（len, len），计算密度低。&lt;/p&gt;
&lt;p&gt;FlashAttention 提出了 1-pass 的 attention 算法，提高了计算密度，缓解了 GPU Memory 到 GPU Cache 的 IO 瓶颈：&lt;/p&gt;</summary>
    
    
    
    <category term="海边拾贝" scheme="https://thysrael.github.io/categories/%E6%B5%B7%E8%BE%B9%E6%8B%BE%E8%B4%9D/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="S10课上" scheme="https://thysrael.github.io/tags/S10%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="海边拾贝" scheme="https://thysrael.github.io/tags/%E6%B5%B7%E8%BE%B9%E6%8B%BE%E8%B4%9D/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-默契</title>
    <link href="https://thysrael.github.io/posts/47f13c89/"/>
    <id>https://thysrael.github.io/posts/47f13c89/</id>
    <published>2025-05-06T12:15:45.000Z</published>
    <updated>2026-02-02T14:42:59.090Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、Kuromi-amp-Melody"><a href="#一、Kuromi-amp-Melody" class="headerlink" title="一、Kuromi &amp; Melody"></a>一、Kuromi &amp; Melody</h2><p>Kuromi 是 AJ 送我的第一个礼物，好像是在五道口地下一层买的，当时我们好像还没有正式确立关系，在回去之前，她走进商店买了 Kuromi，然后挂在了我书包的后面。后面很多的日子里我背上书包，都感觉似乎有一只手在牵着我。</p><p>Melody 是后来我送给 AJ 的，琢磨着可以凑一对。</p><p>嗷对了，AJ 还不相信我能把挂着玩偶的链子解开后再挂到书包上去。</p><p><img src="/posts/47f13c89/2024-03-16_22-14-46_Kuromi&amp;Melody.jpg" alt="2024-03-16_22-14-46_Kuromi&amp;Melody" style="zoom: 50%;"></p><p>我电脑充电器上也有 Kuromi &amp; Melody 的贴纸，也是 AJ 贴上去的。</p><h2 id="二、200-Days"><a href="#二、200-Days" class="headerlink" title="二、200 Days"></a>二、200 Days</h2><p>这是保存在 AJ 手机的一个恋爱计时软件，不过这似乎是我唯一一次见到它。好像 100day 的时候我俩都忘了啥时候开始的了。</p><p>后来好像 300day 的时候关系就有些恶化了，好像就没有再一起过过了。</p><p><img src="/posts/47f13c89/2024-03-16_22-11-16_200day.jpg" alt="2024-03-16_22-11-16_200day" style="zoom:33%;"></p><p>我倒是在 org-mode 中设置了一个 org-habit 来提醒，经常在某些时间点出现在 org-agenda 的第一行。写完这篇文章，我去把它彻底关上。</p><h2 id="三、Tux"><a href="#三、Tux" class="headerlink" title="三、Tux"></a>三、Tux</h2><p>AJ 的生日礼物，小企鹅，本来想送只小鸭子的，但是找半天也没有找到。</p><p><img src="/posts/47f13c89/2024-03-16_22-09-49_微信图片_20240316215816.jpg" alt="2024-03-16_22-09-49_微信图片_20240316215816" style="zoom:33%;"></p><p>其实我也不知道送些什么合适，其实我不觉得这个礼物很能代表我的心意。我在害怕。</p><p>后来这只企鹅被 AJ 放在考研的那个桌斗里，时不时会拿出来。</p><h2 id="四、地坛"><a href="#四、地坛" class="headerlink" title="四、地坛"></a>四、地坛</h2><p>和 AJ 吃到了心心念念的打卤面，在雍和宫附近的“锅儿挑”吃得。不过有一说一只是普通的一碗面条，并没有想象中的好吃。不过他家的老豆腐超级好吃，豆腐紧致，豆香浓郁，蘸着调好的酱油醋非常鲜：</p><p><img src="/posts/47f13c89/2024-03-17_22-00-27_锅儿挑打卤面.jpg" alt="2024-03-17_22-00-27_锅儿挑打卤面" style="zoom:50%;"></p><p>这面确实不如我姥姥家里面做得好吃，店主人谱子还大，AJ 只是吐槽了几句，我念着 AJ 没有直接拂袖而去，也没有很生气。</p><p>不知道为什么，似乎我心里的“北京”在有 AJ 在场的时候总是很丢脸。</p><p>在地坛边上吃了三元梅园的豆沙奶卷，第一口觉的有些淡，但是后面混合甜甜的豆沙吃非常好吃：</p><p><img src="/posts/47f13c89/2024-03-17_22-01-29_豆沙奶卷.jpg" alt="2024-03-17_22-01-29_豆沙奶卷" style="zoom:50%;"></p><p>这个豆沙现在想起来实在是太好吃了，惹得我直流哈喇子。哦对了我想起来当时 AJ 吃得时候两只腿摆来摆去的，我当时一直在想为什么不是在地坛那里摆。</p><p>去了史铁生提到的地坛，在那个台子上坐了很久：</p><p><img src="/posts/47f13c89/dt.jpg" alt="2024-03-17_22-01-49_地坛"></p><p>那段时间我因为学习的事情一直心情不好，现在回看，似乎心情一直没有再好起来过。</p><h2 id="四、Free-Gundam"><a href="#四、Free-Gundam" class="headerlink" title="四、Free Gundam"></a>四、Free Gundam</h2><p>情人节 AJ 送的强袭自由，我还记得我小时候玩 SD 的时候，觉得它们的头都好大，现在玩起来感觉头也不是那么大，可能是手大了的缘故吧。</p><p><img src="/posts/47f13c89/2024-03-26_21-21-09_free.jpg" alt="2024-03-26_21-21-09_free" style="zoom:50%;"></p><p>其实当时我最担心的是她买成了 MGSD，那个好贵的。</p><p>所以人是没有办法走进对方的内心的，你跟别人形容你是怎么样的，别人就会把你当成什么样子。</p><p>（上面这句话就是我在放屁，AJ 最后真的送了我一个 MGSD 的巴巴托斯，我最喜欢的模型）所以人还是有办法走进对方的内心的，只需要许多许多的努力和坚持。</p><h2 id="五、安妮意大利餐厅"><a href="#五、安妮意大利餐厅" class="headerlink" title="五、安妮意大利餐厅"></a>五、安妮意大利餐厅</h2><p>AJ 选得餐厅总是很好吃。我总觉得无论饭菜如何好吃或者难吃，都会过去的，最关键的是要把当时的味道和情感记录下来。但是我当时在写文章的时候，却总有一种落寞之感，我突然觉得我的文字不过是一个酸儒的矫饰，我无法让感情变得更加真挚，也无法让食物变得更加美味。</p><p>超级好吃的意餐（我甚至对着这张照片调了很久，因为果子的颜色被光掩盖了）：</p><p><img src="/posts/47f13c89/2024-04-01_22-53-58_微信图片_20240401225335.jpg" alt="2024-04-01_22-53-58_微信图片_20240401225335" style="zoom:50%;"></p><p>最好吃的是它的蜜瓜火腿，薄火腿片裹着哈密瓜，是一种非常创新的吃法。火腿的咸鲜渗透到哈密瓜瓤的甜蜜中，火腿的果木香与哈密瓜的果气儿缠绵，火腿的绵密和哈密瓜的脆爽交织，给味蕾留下了非常惊艳的记忆。</p><p><img src="/posts/47f13c89/2024-04-01_23-01-20_微信图片_20240401211548.jpg" alt="2024-04-01_23-01-20_微信图片_20240401211548" style="zoom: 50%;"></p><p>我第一次见火腿还是在《鹿鼎记》中，韦小宝用云南宣威火腿调戏小郡主的情节，配合上太监的旁白，就感觉非常好吃。虽然故事里是煮火腿，我们吃得是生火腿，但是其中意趣，也未必没有异曲同工的地方。</p><blockquote><p>韦小宝用筷子挟了一片鲜红喷香的宣威火腿，凑到小郡主口边，笑道：</p><p>“张开嘴来！”</p><p>小郡主牙齿咬实，紧紧闭嘴。</p><p>韦小宝将火腿在她嘴唇上擦来擦去，擦得满嘴是油……</p><p>小太监又送饭菜过来，道：</p><p>“桂公公，这宣威火腿是用蜜饯莲子煮的，煮得急了，或许不很软，请公公包涵。”</p></blockquote><p>与火腿相比，吃千层面主要是图个新鲜，很好奇加菲猫最爱吃的食物是什么样子的。千层面和千层饼并不像，实际上一层肉酱一层面饼，大概有个三四层，上面在铺一层芝士：</p><p><img src="/posts/47f13c89/2024-04-01_23-09-58_微信图片_20240401211558.jpg" alt="2024-04-01_23-09-58_微信图片_20240401211558" style="zoom:50%;"></p><p>我们还点了一个海鲜米饭，里面的虾和蛏子处理得很好，没有什么腥味，AJ 说起意餐的米饭总是要多煮一会儿，糯唧唧的就像粥泡饭一样，这我倒是不知道了，我只是觉得千层面，海鲜米饭和意大利面的味道都很近似，都是那种芝士奶油皮香和番茄肉酱的酸咸味儿。因为有火腿珠玉在前，便觉得后面两道菜逊色了一些：</p><p><img src="/posts/47f13c89/2024-04-01_23-14-40_微信图片_20240401211629.jpg" alt="2024-04-01_23-14-40_微信图片_20240401211629" style="zoom: 25%;"></p><p>其实餐前还上了免费的面包，可以蘸着紫苏酱、黄油和一种红红的酱吃。我觉得口感有些过于“粉”了，我不是很喜欢。</p><p>为了查那种红红的酱是啥，我翻了翻评论，没想到翻到了高晓松似乎是这里的常客。不过我查了查，《同桌的你》是 1993 就写完了，安妮是 1996 年才创建的，所以应该是假的？</p><p><img src="/posts/47f13c89/2024-04-01_23-25-15_微信图片_20240401232328.jpg" alt="2024-04-01_23-25-15_微信图片_20240401232328" style="zoom: 33%;"></p><p>我已经很久不和人说我喜欢听《同桌的你》了。</p><h2 id="六、B-Robot"><a href="#六、B-Robot" class="headerlink" title="六、B-Robot"></a>六、B-Robot</h2><p>这个铁甲小宝本来是我在贵州就看上了，就在 AJ 念高中对面的那个商场。</p><p>后来好像快毕业的时候我买了它。当时 AJ 很激动，我也不知道为什么，就拉着我在旗杆下面拼完了</p><p><img src="/posts/47f13c89/2024-04-29_23-58-42_微信图片_20240429235534.jpg" alt="2024-04-29_23-58-42_微信图片_20240429235534" style="zoom: 25%;"></p><p>拿起来也超级可爱（谁能想到左边才是我的手呢）：</p><p><img src="/posts/47f13c89/2024-04-29_23-59-36_微信图片_20240429235520.jpg" alt="2024-04-29_23-59-36_微信图片_20240429235520" style="zoom:25%;"></p><h2 id="七、真爱小熊"><a href="#七、真爱小熊" class="headerlink" title="七、真爱小熊"></a>七、真爱小熊</h2><p>过生日 AJ 送的真爱小熊。巧克力很好吃，嘎嘎嘎！</p><p><img src="/posts/47f13c89/2024-05-16_16-17-01_img_20240516161626.jpg" alt="2024-05-16_16-17-01_img_20240516161626" style="zoom: 25%;"></p><p>唉，哈哈哈哈哈哈。</p><hr><h2 id="八、总结"><a href="#八、总结" class="headerlink" title="八、总结"></a>八、总结</h2><p>光标悬了很久，我也不知道打什么字。</p><p>可能缺少一些运气和默契吧。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、Kuromi-amp-Melody&quot;&gt;&lt;a href=&quot;#一、Kuromi-amp-Melody&quot; class=&quot;headerlink&quot; title=&quot;一、Kuromi &amp;amp; Melody&quot;&gt;&lt;/a&gt;一、Kuromi &amp;amp; Melody&lt;/h2&gt;&lt;p&gt;Kuromi 是 AJ 送我的第一个礼物，好像是在五道口地下一层买的，当时我们好像还没有正式确立关系，在回去之前，她走进商店买了 Kuromi，然后挂在了我书包的后面。后面很多的日子里我背上书包，都感觉似乎有一只手在牵着我。&lt;/p&gt;
&lt;p&gt;Melody 是后来我送给 AJ 的，琢磨着可以凑一对。&lt;/p&gt;
&lt;p&gt;嗷对了，AJ 还不相信我能把挂着玩偶的链子解开后再挂到书包上去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/47f13c89/2024-03-16_22-14-46_Kuromi&amp;amp;Melody.jpg&quot; alt=&quot;2024-03-16_22-14-46_Kuromi&amp;amp;Melody&quot; style=&quot;zoom: 50%;&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    <category term="S10课上" scheme="https://thysrael.github.io/tags/S10%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
  <entry>
    <title>自由王国-没有银弹</title>
    <link href="https://thysrael.github.io/posts/afa7becb/"/>
    <id>https://thysrael.github.io/posts/afa7becb/</id>
    <published>2025-03-30T02:32:58.000Z</published>
    <updated>2025-08-15T14:36:46.875Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>这是一篇记录我配置新电脑，在新电脑上配置系统的过程。</p><p>之所以取名叫作“没有银弹”，是因为我被挑选硬件、软件的过程折磨的精疲力竭。“没有银弹”虽然是一个老生常谈的话题，但是发现它出现在非软件工程领域，还是非常让人意外的。</p><p>没有一个笔记本、可以兼具 PC 的量大管饱和 Macbook 的高续航和漂亮屏幕；没有一个操作系统，可以兼具 Linux 的开发环境相似性、Windows 的普世和 MacOS 的闭合生态；没有一个发行版，可以兼具 ArchLinux 的高可控性和及时性，NixOS 的可复现性，Ubuntu 的稳定性；没有一个图形服务器兼具 Xorg 的适配性和 Wayland 的高性能；甚至没有一个编辑器，可以兼具 Emacs 的键盘操作和 VSCode 的 Remote 功能！</p><p>在我各种权衡利弊下，我得到了一个低续航、很沉、没有独显、甚至还不便宜的笔记本；上面装着的操作系统，无法驱动触摸板、声卡和蓝牙；软件包管理形式依然是 <code>pacman</code>，只要我开始更新，我就要一直更新；依然是老旧的 Xorg，每使用一天就是在向死亡奔赴一天；都 2025 了，我甚至还要被 Emacs 的沉没成本绑架以至于无法使用 VSCode。</p><p>这岂止是“没有银弹”，这完全是“无法胜利”！我只能忍受这些事情，关键是我都不知道我为什么要忍受这些。我想起了我第一次看见学长在 Manjaro 的 Konsole 上敲命令的样子；我想起了同学开着 VSCode 打开 WSL 的样子；我想起了师兄一只手端着 Mac 去找老师讨论的样子。是的，我羡慕他们。但是又不止是这些，我想到我作为一个 system 方向的研究生，我连我自己的 Operating System 都无法控制；我想到了我第一次编译 Linux 内核的时候，哗哗滚动的命令行；我想到了我的 Emacs 可以显示出来文件图标的样子。</p><p>所以到底为什么？我为什么要执着于这些事情？我在执着于我自己。如果我放弃这些东西，我还是我自己吗？我管计算机世界叫作“自由王国”，因为我相信只要愿意付出精力和时间，那么就会获得自由。可是这些事情告诉我，不是这样的。我无法胜利，我无法自由，在我剩余的生命中，只有一次又一次的妥协。无数次妥协、无数次失败的我，还有自由意志可言吗？而失去自由意志的我，还是我吗？</p><p>如果，“没有银弹”这个事情，不止在“软件工程”和“装新电脑”这两件事情上发生呢？或许在爱情、事业、家庭中，它们都在发生……我到底在执着什么呢？</p><p>我想用我的余生回答这个问题。</p><hr><h2 id="二、Why-Not"><a href="#二、Why-Not" class="headerlink" title="二、Why Not?"></a>二、Why Not?</h2><p>这里会记录我为什么不做一些选择。</p><h3 id="2-1-Wayland"><a href="#2-1-Wayland" class="headerlink" title="2.1 Wayland"></a>2.1 Wayland</h3><p>目前 wayland 不支持腾讯会议的屏幕共享功能，这导致它没有办法作为一个办公本出现。此外 wayland 也不支持类似于 peek 这样的屏幕录制 gif 工具，我也很需要这种工具。</p><h3 id="2-2-Other-OS"><a href="#2-2-Other-OS" class="headerlink" title="2.2 Other OS"></a>2.2 Other OS</h3><h4 id="2-2-1-NixOS"><a href="#2-2-1-NixOS" class="headerlink" title="2.2.1 NixOS"></a>2.2.1 NixOS</h4><p>本次装机因为机子太新了，所以 NixOS 的网卡驱动一直装不上去。</p><p>此外 NixOS 除了可复现性以外，它还提供了一套声明式配置的设计理念。虽然声明式可以有效避免配置项本身的变化，但是我觉得用 Linux，不就是为了用它命令式“刀耕火种“、”手搓系统“的爽感嘛？如果啥啥都要去查 NixOS 的配置文档，那又有什么意思呢？</p><h4 id="2-2-2-Ubuntu"><a href="#2-2-2-Ubuntu" class="headerlink" title="2.2.2 Ubuntu"></a>2.2.2 Ubuntu</h4><p>太旧了，软件更新实在是太不及时了，我不想用一个 26 版本的 Emacs。</p><h3 id="2-3-KDE"><a href="#2-3-KDE" class="headerlink" title="2.3 KDE"></a>2.3 KDE</h3><p>我想要版本监控我的 DE 配置，但是 KDE 的配置文件近乎二进制，很难监控。</p><h3 id="2-4-14-Inch"><a href="#2-4-14-Inch" class="headerlink" title="2.4 14 Inch"></a>2.4 14 Inch</h3><p>没啥，我一个 186 的壮汉，岂能拿这种小家子气的机子。</p><hr><h2 id="三、Setup"><a href="#三、Setup" class="headerlink" title="三、Setup"></a>三、Setup</h2><h3 id="3-1-制作装机盘"><a href="#3-1-制作装机盘" class="headerlink" title="3.1 制作装机盘"></a>3.1 制作装机盘</h3><p>在这个 <a href="https://archlinux.org/download/">页面</a> 下载 ArchLinux ISO 镜像文件，并使用 <code>dd</code> 命令制作装机盘：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">dd</span> <span class="token assign-left variable">bs</span><span class="token operator">=</span>4M <span class="token assign-left variable">if</span><span class="token operator">=</span>/path/to/archlinux.iso <span class="token assign-left variable">of</span><span class="token operator">=</span>/dev/sdx <span class="token assign-left variable">status</span><span class="token operator">=</span>progress <span class="token assign-left variable">oflag</span><span class="token operator">=</span>sync<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>其中 <code>status</code> 用于显示 <code>dd</code> 进程，<code>oflay</code> 用于确保写入同步。</p><h3 id="3-2-Live-环境"><a href="#3-2-Live-环境" class="headerlink" title="3.2 Live 环境"></a>3.2 Live 环境</h3><p>Live 环境指的是采用 U 盘引导进入的系统，当我们把系统安装到主机上，这个过程就结束了（就可以拔 U 盘了）。</p><h4 id="3-2-1-分区前"><a href="#3-2-1-分区前" class="headerlink" title="3.2.1 分区前"></a>3.2.1 分区前</h4><p>reflector 会为你选择速度合适的镜像源，但其结果并不准确，同时会清空配置文件中的内容，对于新人来讲并不适用，我们首先对其进行禁用。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">systemctl stop reflector.service<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>无线连接使用 <code>iwctl</code> 命令进行，按照如下步骤进行网络连接：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">iwctl                           <span class="token comment"># 执行 iwctl 命令，进入交互式命令行</span>device list                     <span class="token comment"># 列出设备名，比如无线网卡看到叫 wlan0</span>station wlan0 scan              <span class="token comment"># 扫描网络</span>station wlan0 get-networks      <span class="token comment">#列出网络 比如想连接 YOUR-WIRELESS-NAME 这个无线</span>station wlan0 connect YOUR-WIRELESS-NAME <span class="token comment"># 进行连接 输入密码即可</span><span class="token builtin class-name">exit</span>                            <span class="token comment"># 成功后 exit 退出</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以等待几秒等网络建立链接后再进行下面测试网络的操作。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">ping</span> www.gnu.org<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>如果不能正常使用网络，那么要确定网卡是否 <code>up</code></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">ip</span> <span class="token function">link</span><span class="token function">ip</span> <span class="token function">link</span> <span class="token builtin class-name">set</span> wlan0 up<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>如果还不行，其实可以将手机和电脑连接在一起，在手机中选择“共享网络”给电脑。</p><p>更新系统时钟：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">timedatectl set-ntp <span class="token boolean">true</span>    <span class="token comment"># 将系统时间与网络时间进行同步</span>timedatectl status          <span class="token comment"># 检查服务状态</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>主要是查看 <code>System clock synchronized</code> 和 <code>NTP Service</code> 这两项，时区设置在后面。</p><h4 id="3-2-2-分区"><a href="#3-2-2-分区" class="headerlink" title="3.2.2 分区"></a>3.2.2 分区</h4><p>为了使得 OS 能够安装在硬盘上并正常启动，我们需要对硬盘进行分区。我们可以先使用如下命令查看一下块设备情况：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">lsblk<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>对于 laptop 来说，一般是有 <code>sda</code> 对应 U 盘，<code>nvme0n1</code> 对应硬盘。我们要对 <code>nvme0n1</code> 进行分区：</p><ul><li>boot 分区：也就是 UEFI 启动时需要的分区，需要 FAT32 格式。</li><li>swap 分区：当物理内存溢出时，可以交换到硬盘上的这个分区。此外休眠（Hibernate）时的状态文件也会保存在这个分区。</li><li><code>/</code> 分区：主分区</li><li><code>/home</code> 分区：家目录分区</li></ul><p>其实没有 swap 分区也可以，目前 laptop 的大内存一般也不会出现溢出的情况。即使需要 Hibernate 的功能，也可以用 swapfile 机制代替。但是我觉得 swapfile 太丑了，而且据说性能不如 swap 分区好，我就选择了 swap 分区。</p><p>此外还有要不要对 <code>/home</code> 单独分区的问题。对 <code>/home</code> 单独分区，可以隔绝系统软件和用户目录对彼此的影响，还可以方便地更换发行版。我之前出现过一次差点重装系统的事故，如果对 <code>/home</code> 单独分区，可以在重装系统的时候避免损坏家目录中的数据文件。</p><p>在进行分区前，需要创建 GPT 分区表：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">lsblk                       <span class="token comment"># 显示分区情况 找到你想安装的磁盘名称</span><span class="token function">parted</span> /dev/nvme0n1         <span class="token comment"># 执行 parted ，进入交互式命令行，进行磁盘类型变更</span><span class="token punctuation">(</span>parted<span class="token punctuation">)</span> mktable            <span class="token comment"># 输入 mktable</span>New disk label type? gpt    <span class="token comment"># 输入 gpt 将磁盘类型转换为 gpt 如磁盘有数据会警告，输入 yes 即可</span>quit                        <span class="token comment"># 最后 quit 退出 parted 命令行交互</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后执行如下命令进入 TUI 界面对硬盘进行分区：</p><pre class="line-numbers language-none"><code class="language-none">cfdisk /dev/nvme0n1<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>界面非常简单，用方向键和回车就可以操作，分区的具体信息如下表所示：</p><div class="table-container"><table><thead><tr><th>Partition</th><th>Size</th><th>Type</th></tr></thead><tbody><tr><td>boot</td><td>512 MiB</td><td>EFI System</td></tr><tr><td>swap</td><td>16 GiB</td><td>Linux Swap</td></tr><tr><td><code>/</code></td><td>144 GiB</td><td>Linux filesystem</td></tr><tr><td><code>/home</code></td><td>793.4 GiB</td><td>Linux filesystem</td></tr></tbody></table></div><p>GB 是以 10 为基数的，厂商宣传一般用这个，而 GiB 是以 2 为基数的，更适合 SSD 硬件体质。</p><p>关于 <code>/</code> 的大小，我使用了 5 年的笔记本大约是 120G，其中有 50G 的 cache 还没有清理，所以我觉得 144G 是一个比较合理的数值。而关于 swap 分区的大小，纯粹是我舍不得了。</p><p>使用如下命令复查分区情况：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">fdisk</span> -l<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后格式化分区：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># boot</span>mkfs.vfat -F <span class="token number">32</span> /dev/nvme0n1p1<span class="token comment"># swap</span><span class="token function">mkswap</span> /dev/nvme0n1p2<span class="token function">swapon</span> /dev/nvme0n1p2<span class="token comment"># root</span>mkfs.ext4 /dev/nvme0n1p3mkfs.ext4 /dev/nvme0n1p4<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>至于为啥选择了 ext4 而没有选择 btrfs，是因为 ext4 性能好，稳定，兼容性好，虽然 btrfs 功能多，但是那些功能我也不懂，所以就没有选。</p><h4 id="3-2-3-挂载"><a href="#3-2-3-挂载" class="headerlink" title="3.2.3 挂载"></a>3.2.3 挂载</h4><p>要按照顺序对分区进行挂载，先挂载 <code>/</code> 分区：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># root</span><span class="token function">mount</span> /dev/nvme0n1p3 /mnt<span class="token comment"># boot</span><span class="token function">mkdir</span> /mnt/boot<span class="token function">mount</span> /dev/nvme0n1p1 /mnt/boot<span class="token comment"># home</span><span class="token function">mkdir</span> /mnt/home<span class="token function">mount</span> /dev/nvme0n1p4 /mnt/home<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后修改镜像源，来为安装软件做准备：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">vim</span> /etc/pacman.d/mirrorlist<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h4 id="3-2-4-初步安装与配置"><a href="#3-2-4-初步安装与配置" class="headerlink" title="3.2.4 初步安装与配置"></a>3.2.4 初步安装与配置</h4><p>本质是把中国的源往前提：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">Server <span class="token operator">=</span> https://mirrors.ustc.edu.cn/archlinux/<span class="token variable">$repo</span>/os/<span class="token variable">$arch</span>Server <span class="token operator">=</span> https://mirrors.tuna.tsinghua.edu.cn/archlinux/<span class="token variable">$repo</span>/os/<span class="token variable">$arch</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>然后安装基础的包：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacstrap /mnt base base-devel linux linux-headers linux-firmware  <span class="token comment"># base-devel 在 AUR 包的安装是必须的</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>安装必要的功能性软件</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacstrap /mnt dhcpcd iwd <span class="token function">vim</span> bash-completion   <span class="token comment"># 有线所需(iwd 也需要 dhcpcd )、无线所需、编辑器、补全工具</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>生成 fstab 文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">genfstab -U /mnt <span class="token operator">&gt;&gt;</span> /mnt/etc/fstab<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>chroot</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">arch-chroot /mnt<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>设置时区：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">ln</span> -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimehwclock --systohc<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>然后设置 locale，它决定了地域、货币、时区日期的格式、字符排列方式和其他本地化标准。先用 vim 去掉 <code>/etc/locale.gen</code> 文件中的  <code>en_US.UTF-8</code> 和 <code>zh_CN.UTF-8</code> 的注释。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">vim</span> /etc/locale.gen<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后使用如下命令生成 locale：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">locale-gen<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>最后向 <code>/etc/locale.conf</code> 导入内容</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">'LANG=en_US.UTF-8'</span>  <span class="token operator">&gt;</span> /etc/locale.conf<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后在 <code>/etc/hostname</code> 设置主机名：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">vim</span> /etc/hostname<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接下来在<code>/etc/hosts</code> 设置与其匹配的条目。</p><pre class="line-numbers language-none"><code class="language-none">vim /etc/hosts<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>加入如下内容：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token number">127.0</span>.0.1   localhost::1         localhost<span class="token number">127.0</span>.1.1   loquat<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>然后为 root 设置密码：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">passwd</span> root<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后安装微码：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacman -S intel-ucode   <span class="token comment"># Intel</span>pacman -S amd-ucode     <span class="token comment"># AMD</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h4 id="3-2-5-GRUB"><a href="#3-2-5-GRUB" class="headerlink" title="3.2.5 GRUB"></a>3.2.5 GRUB</h4><p>首先安装引导程序：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># grub 是启动引导器，efibootmgr 被 grub 脚本用来将启动项写入 NVRAM</span>pacman -S grub efibootmgrgrub-install --target<span class="token operator">=</span>x86_64-efi --efi-directory<span class="token operator">=</span>/boot --bootloader-id<span class="token operator">=</span>GRUB<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>然后通过编辑 <code>/etc/default/grub</code> 文件，配置 grub 传递给 Linux Kernel 的参数：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">GRUB_CMDLINE_LINUX_DEFAULT</span><span class="token operator">=</span><span class="token string">"loglevel=5 nowatchdog"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>增加 log level 有助于 debug， <code>nowatchdog</code> 可以提高启动速度。</p><p>然后让 grub 生成配置文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">grub-mkconfig</span> -o /boot/grub/grub.cfg<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h4 id="3-2-6-完成安装"><a href="#3-2-6-完成安装" class="headerlink" title="3.2.6 完成安装"></a>3.2.6 完成安装</h4><p>退出 <code>chroot</code> 环境，重启：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token builtin class-name">exit</span>                <span class="token comment"># 退回安装环境#</span><span class="token function">umount</span> -R  /mnt     <span class="token comment"># 卸载新分区</span><span class="token function">reboot</span>              <span class="token comment"># 重启</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>重启后启动 dhcp 和无线网络服务</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">systemctl start dhcpcdsystemctl start iwd<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>然后就可以使用 <code>iwctl</code> 工具来连接网络了。</p><h3 id="3-3-开始之前"><a href="#3-3-开始之前" class="headerlink" title="3.3 开始之前"></a>3.3 开始之前</h3><p>在我们开始安装各种应用软件之前，我们还需要做一些准备工作：</p><h4 id="3-3-1-创建用户"><a href="#3-3-1-创建用户" class="headerlink" title="3.3.1 创建用户"></a>3.3.1 创建用户</h4><p>创建一个普通用户 thysrael：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">useradd</span> -m -G wheel -s /bin/bash thysrael  <span class="token comment"># wheel 附加组可使用 sudo，-m 同时创建用户家目录</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>设置 thysrael 的密码：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">passwd</span> thysreal<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>编辑 sudoers 配置文件</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">EDITOR</span><span class="token operator">=</span>vim visudo  <span class="token comment"># 需要以 root 用户运行 visudo 命令</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>找到下面这样的一行，把前面的注释符号 <code>#</code> 去掉，<code>:wq</code> 保存并退出即可。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment">#%wheel ALL=(ALL:ALL) ALL</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h4 id="3-3-2-网络连接"><a href="#3-3-2-网络连接" class="headerlink" title="3.3.2 网络连接"></a>3.3.2 网络连接</h4><p>我们在 live 环境下使用的网络连接工具是 <code>dhcpcd</code> 配合 <code>iwd</code>，但是在日常使用中，我们更喜欢 <code>networkmanager</code>，因为它的使用更加简便，而且和 GUI 界面融合的更好。所以我们安装 <code>networkmanager</code> 并关闭 <code>dhcpcd</code> 和 <code>iwd</code> 的功能。</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> pacman -S networkmanager<span class="token function">sudo</span> systemctl disable iwd                             <span class="token comment"># 确保 iwd 开机处于关闭状态，其无线连接会与 NetworkManager 冲突</span><span class="token function">sudo</span> systemctl stop iwd                                <span class="token comment"># 同上，立即关闭 iwd</span><span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> --now NetworkManager             <span class="token comment"># 确保先启动 NetworkManager，并进行网络连接</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>可能需要重启才能生效。</p><p>我们可以使用如下命令在 CLI 界面使用 networkmanager：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># 列出所有设备</span>nmcli device<span class="token comment"># 显示所有可用的 Wi-Fi 网络</span>nmcli device wifi list<span class="token comment"># 连接到一个 Wi-Fi 网络</span>nmcli device wifi connect <span class="token string">"SSID_NAME"</span> password <span class="token string">"your_password"</span><span class="token comment"># 断开网络连接</span>nmcli device disconnect iface_name<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>应该配置一次后就可以自动连接了。</p><h4 id="3-3-3-包管理器"><a href="#3-3-3-包管理器" class="headerlink" title="3.3.3 包管理器"></a>3.3.3 包管理器</h4><p>首先我们先开启 32 位支持库（我也不知道为啥，可能这样包的数量就变多了吧），编辑 <code>/etc/pacman.conf</code> 将 <code>[multilib]</code> 那一节的注释取消掉。</p><p>然后使用如下命令刷新 <code>pacman</code> 数据库：</p><pre class="line-numbers language-shell" data-language="shell"><code class="language-shell">pacman -Syyu<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后我们要安装 <code>yay</code> ，目前 <code>yay</code> 被墙了，所以我们要先安装 <code>git</code> ，然后 clone 下来 <code>yay</code> 仓库并构建：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacman -S <span class="token function">git</span><span class="token function">git</span> clone https://aur.archlinux.org/yay-bin.git <span class="token comment"># 一定得是 yay-bin，因为 yay 基于 go 构建，go 也被墙了</span><span class="token builtin class-name">cd</span> yay-binmakepkg -si<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><h3 id="3-4-GUI"><a href="#3-4-GUI" class="headerlink" title="3.4 GUI"></a>3.4 GUI</h3><p>我们要安装 i3 作为我们的窗口管理器。</p><h4 id="3-4-1-显卡驱动"><a href="#3-4-1-显卡驱动" class="headerlink" title="3.4.1 显卡驱动"></a>3.4.1 显卡驱动</h4><p>首先安装 Intel 集成显卡驱动：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacman -S mesa lib32-mesa vulkan-intel lib32-vulkan-intel<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><ul><li><code>mesa</code> 是开源的 OpenGL 实现，支持 Intel 核显的 3D 图形渲染。</li><li><code>Vulkan</code> 是核显的 Vulkan API 驱动，支持现代图形渲染技术（如光线追踪、高性能计算）。</li></ul><p>不建议安装 <code>xf86-video-intel</code> ，似乎说性能不好。</p><h4 id="3-4-2-Xorg"><a href="#3-4-2-Xorg" class="headerlink" title="3.4.2 Xorg"></a>3.4.2 Xorg</h4><p>安装 xorg 作为我们的图形服务器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacman -S xorg-server<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><code>xorg-server</code> ：是一个开源的 X Window System 服务器，用于管理图形显示和用户输入。</p><h4 id="3-4-3-登录器"><a href="#3-4-3-登录器" class="headerlink" title="3.4.3 登录器"></a>3.4.3 登录器</h4><p>我们需要先安装登录器，这样我们一开机就可以进入 GUI 了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> pacman -S sdm<span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> sdm.service<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>本来想安装 lightdm 的，结果启动不成功。</p><h4 id="3-4-4-i3wm"><a href="#3-4-4-i3wm" class="headerlink" title="3.4.4 i3wm"></a>3.4.4 i3wm</h4><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> pacman -S i3-wm i3status dmenu<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><ul><li><p><strong>i3-wm</strong> 是一个高度可定制、平铺式窗口管理器。</p></li><li><p><strong>i3status</strong> 是一个简单的状态栏生成器，它可以在 i3-wm 的栏上显示有关系统状态（如时间、电池状态、网络连接等）的信息。</p></li><li><strong>dmenu</strong> 是一个轻量级的动态菜单。它可以用于启动应用程序、执行命令或选择其他操作。</li></ul><p>此外，为了让我们进入 I3 后有个终端模拟器可以用，我们还安装 kitty：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> pacman -S kitty<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然后我们重启就可以了。</p><h3 id="3-5-硬件适配"><a href="#3-5-硬件适配" class="headerlink" title="3.5 硬件适配"></a>3.5 硬件适配</h3><h4 id="3-5-1-改键位"><a href="#3-5-1-改键位" class="headerlink" title="3.5.1 改键位"></a>3.5.1 改键位</h4><p>然后我们就可以安装我心心念念的改键器了：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pacman -S interception-caps2esc<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>安装好了以后，在<code>/etc/udevmon.yaml</code>添加下列代码</p><pre class="line-numbers language-yaml" data-language="yaml"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">JOB</span><span class="token punctuation">:</span> <span class="token string">"intercept -g $DEVNODE | caps2esc | uinput -d $DEVNODE"</span>  <span class="token key atrule">DEVICE</span><span class="token punctuation">:</span>     <span class="token key atrule">EVENTS</span><span class="token punctuation">:</span>       <span class="token key atrule">EV_KEY</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>KEY_CAPSLOCK<span class="token punctuation">,</span> KEY_ESC<span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>在 <code>/etc/systemd/system/udevmon.service</code> 中添加下列代码</p><pre class="line-numbers language-toml" data-language="toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token table class-name">Unit</span><span class="token punctuation">]</span><span class="token key property">Description</span><span class="token punctuation">=</span>udevmon<span class="token key property">Wants</span><span class="token punctuation">=</span>systemd-udev-settle<span class="token punctuation">.</span>service<span class="token key property">After</span><span class="token punctuation">=</span>systemd-udev-settle<span class="token punctuation">.</span>service<span class="token punctuation">[</span><span class="token table class-name">Service</span><span class="token punctuation">]</span><span class="token key property">ExecStart</span><span class="token punctuation">=</span>/usr/bin/nice -n <span class="token number">-20</span> /usr/bin/udevmon -c /etc/udevmon<span class="token punctuation">.</span>yaml<span class="token punctuation">[</span><span class="token table class-name">Install</span><span class="token punctuation">]</span><span class="token key property">WantedBy</span><span class="token punctuation">=</span>multi-user<span class="token punctuation">.</span>target<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>最后使用如下命令来启动：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> --now udevmon<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>最终还是转成 Mac 了。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;这是一篇记录我配置新电脑，在新电脑上配置系统的过程。&lt;/p&gt;
&lt;p&gt;之所以取名叫作“没有银弹”，是因为我被挑选硬件、软件的过程折磨的精疲力竭。“没有银弹”虽然是一个老生常谈的话题，但是发现它出现在非软件工程领域，还是非常让人意外的。&lt;/p&gt;
&lt;p&gt;没有一个笔记本、可以兼具 PC 的量大管饱和 Macbook 的高续航和漂亮屏幕；没有一个操作系统，可以兼具 Linux 的开发环境相似性、Windows 的普世和 MacOS 的闭合生态；没有一个发行版，可以兼具 ArchLinux 的高可控性和及时性，NixOS 的可复现性，Ubuntu 的稳定性；没有一个图形服务器兼具 Xorg 的适配性和 Wayland 的高性能；甚至没有一个编辑器，可以兼具 Emacs 的键盘操作和 VSCode 的 Remote 功能！&lt;/p&gt;
&lt;p&gt;在我各种权衡利弊下，我得到了一个低续航、很沉、没有独显、甚至还不便宜的笔记本；上面装着的操作系统，无法驱动触摸板、声卡和蓝牙；软件包管理形式依然是 &lt;code&gt;pacman&lt;/code&gt;，只要我开始更新，我就要一直更新；依然是老旧的 Xorg，每使用一天就是在向死亡奔赴一天；都 2025 了，我甚至还要被 Emacs 的沉没成本绑架以至于无法使用 VSCode。&lt;/p&gt;</summary>
    
    
    
    <category term="自由王国" scheme="https://thysrael.github.io/categories/%E8%87%AA%E7%94%B1%E7%8E%8B%E5%9B%BD/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="自由王国" scheme="https://thysrael.github.io/tags/%E8%87%AA%E7%94%B1%E7%8E%8B%E5%9B%BD/"/>
    
    <category term="s10课上" scheme="https://thysrael.github.io/tags/s10%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
  <entry>
    <title>海边拾贝-FuseMax</title>
    <link href="https://thysrael.github.io/posts/ce241189/"/>
    <id>https://thysrael.github.io/posts/ce241189/</id>
    <published>2025-02-13T13:32:06.000Z</published>
    <updated>2025-08-15T12:16:48.910Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这篇工作比较缝合，它引用了 3 个 idea：</p><ul><li><a href="https://arxiv.org/abs/2205.14135">Flash Attention</a> 和 pass 抽象</li><li><a href="https://dl.acm.org/doi/10.1145/3613424.3623791">TeAAL</a> 和它使用的 Einsum 规范</li><li><a href="https://arxiv.org/abs/2404.11591">Extended Einsum</a></li></ul><p>TeAAL 提出用 Einsum 的形式去描述算子，并指导加速器的设计。但是 Einsum 只能描述仅包含加法和乘法的算子，对于 Flash-Attension 这种包含指数运算（softmax）和迭代运算的算子无法描述，于是作者借用了 Extended Einsum 的形式描述了 Flash Attension 算子并实现了 FuseMax 加速器，同时论证了 pass 抽象的合理性。</p><p>而实际上，TeAAL 提出的用 Einsum 指导加速器设计的思路并没有因为使用了 Extended Einsum 而被拓展；FuseMax 所展现的性能优势，大部分来自于 Flash-Attension 算法本身，而不是其硬件实现。</p></blockquote><h2 id="一、Background"><a href="#一、Background" class="headerlink" title="一、Background"></a>一、Background</h2><h3 id="1-1-Flash-Attention-Pass"><a href="#1-1-Flash-Attention-Pass" class="headerlink" title="1.1 Flash Attention, Pass"></a>1.1 Flash Attention, Pass</h3><p>Flash-Attention 是一种 Attention 算子的实现，相比于传统的实现，它可以降低内存带宽的需求，并且使用更少的片上内存，更适合当前加速器存在的 memory bound。为了达到这个目的，我们需要：</p><ul><li>尽可能少的从内存中读取数据 -&gt; 算法设计的 pass 数要少</li><li>尽可能少使用片上内存 -&gt; tile 后 reduce</li></ul><p>而这两个需求都被 softmax 的传统实现阻止了，softmax 的表达式如下：</p><p><img src="/posts/ce241189/-17394538526611.png" alt="img"></p><p>传统的 softmax 实现是一种 3-pass 的实现：</p><p><img src="/posts/ce241189/-17394538576465.png" alt="img"></p><p>所谓的 pass，就是需要访问输入的次数：</p><blockquote><p>the number of times a given element of an input must be revisited after visiting every other element of the input. </p></blockquote><p>因为 softmax 需要先遍历所有元素计算出 max 值，然后根据 max 值遍历所有元素计算分母，再根据分母计算分子。</p><p>在 flash-attention 之前，有 2018 online-softmax 工作，将算法优化成了 2-pass 的。他将第 1，2 轮进行了合并：</p><p><img src="/posts/ce241189/-17394538620357.png" alt="img"></p><p>最终结果如图：</p><p><img src="/posts/ce241189/-173945386674411.png" alt="img"></p><p>如果仅在 softmax 层，那么 2-pass 就是极限了，不过如果考虑整个 Attention，那么是可以继续优化成 1-pass 的算法，这就是 Flash-Attention，2-pass 的 Attention 表示如下：</p><p><img src="/posts/ce241189/-173945387091113.png" alt="img"></p><p>然后我们注意到（直观上说，是利用了 a 这个数组并不是最终结果，而是会被 reduce 的性质）：</p><p><img src="/posts/ce241189/-173945387591215.png" alt="img"></p><p>整理后得到：</p><p><img src="/posts/ce241189/-173945387855817.png" alt="img"></p><p>这就是一个 1-pass 的 Flash-Attention 算法。在此基础上，如果增加了 tile 操作，那么就会获得完全体的 Flash-Attention，但这本文的重点是对于 pass 的优化。</p><p><img src="/posts/ce241189/-173945388095219.png" alt="img"></p><h3 id="1-2-Einsum-Notation"><a href="#1-2-Einsum-Notation" class="headerlink" title="1.2 Einsum Notation"></a>1.2 Einsum Notation</h3><p>爱因斯坦求和标记（Einstein summation notation）是一种标记的约定，用于描述张量运算。比如说二维矩阵乘法就可以被描述为：</p><p><img src="/posts/ce241189/-173945388316621.png" alt="img"></p><p>而矩阵与向量的乘法可以被描述为：</p><p><img src="/posts/ce241189/-173945388498323.png" alt="img"></p><p>Einsum 的输入包括张量，如<script type="math/tex">A, B</script>和其对应的坐标，如<script type="math/tex">m,k</script>和<script type="math/tex">k,n</script>，还有输出矩阵对应的坐标，如<script type="math/tex">m,</script>，比如在 numpy 中，$$$$和$$$$的矩阵乘法写作：</p><pre class="line-numbers language-Python" data-language="Python"><code class="language-Python">np.einsum('mk,kn-&gt;mn', A, B)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Einsum 的特点在于，它将 compute 和 reduce 阶段完全区分开了，而不是混在一起，更加清晰了。</p><p>具体而言，在 compute 阶段，我们会根据输入坐标，构建一个迭代空间，然后遍历空间中的每一个点进行计算，得到一个相同维度的张量。以矩阵乘法为例，输入坐标是<script type="math/tex">m,k</script>和<script type="math/tex">k,n</script>，我们构建的迭代空间是<script type="math/tex">[1,M] \times [1,K] \times [1,N]</script>。经过计算得到的张量是<script type="math/tex">z'_{m,k, n} = [a_{m,k} \times b_{k,n}]</script>。</p><p>在 reduce 阶段，我们需要将我们得到的张量 $z<em>{m,k,n}$ 与输出坐标 <script type="math/tex">m,n</script> 进行比对，发现多了一个 <script type="math/tex">k</script> 维度。所以我们会沿着多出的维度进行规约，然后就可以得到 $$z</em>{m,n}=[\sum^{K}<em>{k=1}[a</em>{m,k} \times b_{k,n}]$$。</p><p>对比我平时用的矩阵乘法，可以看到 compute 和 reduce 是混合在一起的。</p><pre class="line-numbers language-C" data-language="C"><code class="language-C"> for (int i = 0; i &lt; M; i++) {     for (int j = 0; j &lt; N; j++) {        for (int k = 0; k &lt; K; k++) {            Z[i][j] += A[i][k] * B[k][j];         }    }}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="1-3-TeAAL"><a href="#1-3-TeAAL" class="headerlink" title="1.3 TeAAL"></a>1.3 TeAAL</h3><p>TeAAL 是一个加速器模型 generator，可以根据不同算子生成不同的加速器模型。在它的设计中，算子需要使用 Einsum Cascade 来进行描述，也就是一系列的 Einsum 。</p><p>使用 Einsum 好处在于，引入了迭代空间，使得许多加速器设计中的优化和 tradeoff 都非常清晰。TeAAL 提出了 3 个维度的优化：</p><ul><li><strong>Loop Order</strong>：迭代空间“是<script type="math/tex">[1,M] \times [1,K] \times [1,N]</script>还是<script type="math/tex">[1,K] \times [1,M] \times [1,N]</script>”？这会影响数据是 stationary 的，还是 stream 的。</li><li><strong>Splitting</strong>：运算中我们常常将输入分块计算，也可以视为在对迭代空间分块。</li><li><strong>Work scheduling</strong>：根据迭代空间计算出的张量，是如何摆放的？包括空间和时间维度。</li></ul><p>总之 Einsum 是一个非常适合数学化表述加速器设计的标记。</p><h3 id="1-4-Extended-Einsum"><a href="#1-4-Extended-Einsum" class="headerlink" title="1.4 Extended Einsum"></a>1.4 Extended Einsum</h3><p>传统的 Einsum 是不能指定运算符的，比如说在 Numpy 中，Compute 阶段只能使用乘法，Reduce 阶段只能是加法：</p><pre class="line-numbers language-Python" data-language="Python"><code class="language-Python">np.einsum('mk,kn-&gt;mn', A, B)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p> 而 Extended Einsum 则允许自定义 Compute 和 Reduce 阶段的运算符，例如：</p><p><img src="/posts/ce241189/-173945389006225.png" alt="img"></p><p><script type="math/tex">\bigwedg</script> 是 Compute 阶段使用的运算符，<script type="math/tex">\bigve</script>是 Reduce 阶段使用的运算符。</p><p>除此之外，Extended Einsum 还可以表达循环，比如说前缀和计算：</p><p><img src="/posts/ce241189/-173945389210827.png" alt="img"></p><p>还可以表达递推式，同样是前缀和计算：</p><p><img src="/posts/ce241189/-173945389430429.png" alt="img"></p><p>有了 Extended Einsum，我们就可以描述 Flash-Attention 这种包含许多复杂运算和递推式的算法。</p><hr><h2 id="二、Contribution"><a href="#二、Contribution" class="headerlink" title="二、Contribution"></a>二、Contribution</h2><h3 id="2-1-Einsum-与-Pass"><a href="#2-1-Einsum-与-Pass" class="headerlink" title="2.1 Einsum 与 Pass"></a>2.1 Einsum 与 Pass</h3><p>本文认为，如果将算子写成 Einsum 的形式，那么是有助于确定算子的 Pass 数的，比如说这个算子，就是 2-Pass：</p><p><img src="/posts/ce241189/-173945389672931.png" alt="img"></p><p>而相同功能的另一个算子，就是 1-Pass：</p><p><img src="/posts/ce241189/-173945389876733.png" alt="img"></p><p>本文认为，迭代空间也可以被表示成一个 fibertree，而根据这些 fibertree 就可以判断 pass。首先，我不知道为什么原本适用于稀疏矩阵表示的 fibertree 要用来表示一个非常稠密的迭代空间（它可能是想说 fibertree 用于表示根据迭代空间生成的那个向量），其次，我不知道为什么表示成了 fibertree，就可以看出来是多少个 Pass。</p><p>它原文中对迭代空间的 fibertree 定义如下，非常的简略：</p><blockquote><p>The is-fibertree is a special tree where each fiber belongs to a rank in the iteration space of the Einsum.</p></blockquote><p>而他介绍的根据 fibertree 识别 Pass 的方法，则依赖于非常主观的“Dependency”，其定义基本上和 Flash-Attention 中的定义一样：</p><blockquote><p>Now, in a scenario where fibers for a particular rank exist in multiple is-fibertrees; in each, they project to the same tensor; and <strong>there is a dependency such that all of the elements of the earlier is-fibertree’s fiber must be read before any element can be read again by the later is-fibertree (for all mappings of the</strong> <strong>cascade**</strong>)**, we refer to that read-read sequence as creating an additional pass.</p></blockquote><p>这种冗余和主观定义的方式，指示它无法编写成一个程序来自动优化 Pass 数目：</p><blockquote><p>We leave a full analysis of the space of pass-reduction approaches to future work.</p></blockquote><h3 id="2-2-用-Einsum-表示-Flash-Attention"><a href="#2-2-用-Einsum-表示-Flash-Attention" class="headerlink" title="2.2 用 Einsum 表示 Flash-Attention"></a>2.2 用 Einsum 表示 Flash-Attention</h3><p>用 Extended Einsum 表示 Flash-Attention，结果如图：</p><p><img src="/posts/ce241189/-173945390147135.png" alt="img"></p><p>文章只是将 Flash-Attention 换了一种标记形式（从伪代码到 Einsum Cascade），其算法的实质并没有发生改变。</p><h3 id="2-3-将-Flash-Attention-Map-到硬件上"><a href="#2-3-将-Flash-Attention-Map-到硬件上" class="headerlink" title="2.3 将 Flash-Attention Map 到硬件上"></a>2.3 将 Flash-Attention Map 到硬件上</h3><p>本文将 Flash-Attention 实现到了 Timeloop and Accelergy 模拟的 spatial 架构上：</p><p><img src="/posts/ce241189/-173945390429437.png" alt="img"></p><p>传统的 Attention 加速器，使用 2D Array 来计算矩阵乘法，使用 1D Array 来计算 softmax，这种安排的缺点在于，1D Array 计算 softmax 非常吃力，进而导致 2D Array 需要等待 1D Array 的计算，造成了低计算利用率。</p><p>但是 Flash-Attention 算法本身就融合 softmax 到前面的计算中，所以有一部分的 softmax 的计算任务，是可以放到 2D Array 中计算的，这样两个部分的计算任务就更加均衡了。</p><p>此外，2D Array 的 fill 和 drain 的开销很大，所以需要使用流水线的方法摊还（amortize）开销。</p><hr><h2 id="三、Evaluation"><a href="#三、Evaluation" class="headerlink" title="三、Evaluation"></a>三、Evaluation</h2><h3 id="3-1-Setup"><a href="#3-1-Setup" class="headerlink" title="3.1 Setup"></a>3.1 Setup</h3><p>实验在 TimeLoop 模拟器上进行（全是 Python 代码），BaseLine 分别是一个未经优化的 Attention 加速器，和 FLAT（经过 Fusion 等优化，但是依然使用普通 Attention 算法的加速器）。</p><p>WorkLoad 有 BERT，TrXL，T5，XLM。</p><h3 id="3-2-Compute-Utilization"><a href="#3-2-Compute-Utilization" class="headerlink" title="3.2 Compute Utilization"></a>3.2 Compute Utilization</h3><p>在真实负载情况下，计算单元利用率的比值：</p><p><img src="/posts/ce241189/-173945390670239.png" alt="img"></p><p>正如前文分析的，Baseline 的 2D Array 受到 1D Array 的拖累，导致利用率极低。</p><p>而在序列长度过长时，传统 Attention 算法会导致 global buffer 溢出，进而计算率下降，而 Flash-Attention 则没有这个问题。</p><h3 id="3-3-SpeedUp"><a href="#3-3-SpeedUp" class="headerlink" title="3.3 SpeedUp"></a>3.3 SpeedUp</h3><p>在 attention 时的加速比：</p><p><img src="/posts/ce241189/-173945390896841.png" alt="img"></p><p>在 inference 时的加速比：</p><p><img src="/posts/ce241189/28d63d58-4bd5-4995-ac91-a004c6cf6f85.png" alt=""></p><p>因为计算单元利用率提高，所以加速比也显著提高。</p><h3 id="3-4-Energy"><a href="#3-4-Energy" class="headerlink" title="3.4 Energy"></a>3.4 Energy</h3><p>在 attention 时的能耗：</p><p><img src="/posts/ce241189/-173945391102343.png" alt="img"></p><p>在 inference 时的能耗：</p><p><img src="/posts/ce241189/2861afa9-44ca-43ca-ba57-24cee9103951-1740103737579-3.png" alt="2861afa9-44ca-43ca-ba57-24cee9103951"></p><p>Flash-Attention 节约了 DRAM 的访存开销。</p><hr><h2 id="四、计算密度"><a href="#四、计算密度" class="headerlink" title="四、计算密度"></a>四、计算密度</h2><p>这篇工作主要结合了前人的工作，许多成果本质上是算法（Flash-Attention）或者开发框架（TeAAL，Timeloop）的成果，而非这篇工作自己的成果。</p><p>随着计算单元数目的增多，Roofline 模型中的硬件的 <script type="math/tex">I_{max}</script>越来越往右移动，这就导致越来越多的算法成为 memory bound 的：</p><p><img src="/posts/ce241189/-173945391334345.png" alt="img"></p><p>有一种解决问题的方式是减少内存读取的次数，比如说 Kernel Fusion：</p><p><img src="/posts/ce241189/-173945391528647.png" alt="img"></p><p>直白的 Fusion 并不会修改算子的实现，它只是将计算的中间结果存在了 On-chip Memory 中，但是 On-chip Memory 空间有限，这就导致一旦存不下来，依然会溢出到 Off-chip Memory 中，最终效果并不好（从上文的 Evaluation 中也可以看出）。</p><p>为了解决溢出问题（或者单纯为了减少访存次数），有一种思路是增加 On-chip Memory 的容量，比如说 IPU，相比于 CPU 和 GPU，就增加了更多的片上 SRAM：</p><p><img src="/posts/ce241189/-173945391724649.png" alt="img"></p><p>但是这种方式存在问题，就是片上 SRAM 的面积过大，IPU 很少去和 GPU 对比单位芯片面积（iso-area）下的性能。</p><p>Flash-Attention 和 FuseMax 我认为是另一种思路的代表，就是“以算代存”，计算的中间结果并存储后供后续使用，而是当需要使用的时候，再次计算一遍，是一种更加“数据流”的方法。通过构造额外的计算，来避免存储（Flash Attention 引入了更多的冗余的计算，但是减少了冗余的存储）：</p><p><img src="/posts/ce241189/-173945392031051.png" alt="img"><img src="/posts/ce241189/-173945392212853.png" alt="img"></p><p>我个人隐隐约约感觉，计算单元和存储单元有某种统一性，如果能把握它并提出一种更好的抽象，或许可以做出一个更本质的 tradeoff。</p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这篇工作比较缝合，它引用了 3 个 idea：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2205.14135&quot;&gt;Flash Attention&lt;/a&gt; 和 pass 抽象&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dl.acm.org/doi/10.1145/3613424.3623791&quot;&gt;TeAAL&lt;/a&gt; 和它使用的 Einsum 规范&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2404.11591&quot;&gt;Extended Einsum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TeAAL 提出用 Einsum 的形式去描述算子，并指导加速器的设计。但是 Einsum 只能描述仅包含加法和乘法的算子，对于 Flash-Attension 这种包含指数运算（softmax）和迭代运算的算子无法描述，于是作者借用了 Extended Einsum 的形式描述了 Flash Attension 算子并实现了 FuseMax 加速器，同时论证了 pass 抽象的合理性。&lt;/p&gt;
&lt;p&gt;而实际上，TeAAL 提出的用 Einsum 指导加速器设计的思路并没有因为使用了 Extended Einsum 而被拓展；FuseMax 所展现的性能优势，大部分来自于 Flash-Attension 算法本身，而不是其硬件实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;一、Background&quot;&gt;&lt;a href=&quot;#一、Background&quot; class=&quot;headerlink&quot; title=&quot;一、Background&quot;&gt;&lt;/a&gt;一、Background&lt;/h2&gt;&lt;h3 id=&quot;1-1-Flash-Attention-Pass&quot;&gt;&lt;a href=&quot;#1-1-Flash-Attention-Pass&quot; class=&quot;headerlink&quot; title=&quot;1.1 Flash Attention, Pass&quot;&gt;&lt;/a&gt;1.1 Flash Attention, Pass&lt;/h3&gt;&lt;p&gt;Flash-Attention 是一种 Attention 算子的实现，相比于传统的实现，它可以降低内存带宽的需求，并且使用更少的片上内存，更适合当前加速器存在的 memory bound。为了达到这个目的，我们需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽可能少的从内存中读取数据 -&amp;gt; 算法设计的 pass 数要少&lt;/li&gt;
&lt;li&gt;尽可能少使用片上内存 -&amp;gt; tile 后 reduce&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="海边拾贝" scheme="https://thysrael.github.io/categories/%E6%B5%B7%E8%BE%B9%E6%8B%BE%E8%B4%9D/"/>
    
    
    <category term="S9课上" scheme="https://thysrael.github.io/tags/S9%E8%AF%BE%E4%B8%8A/"/>
    
    <category term="海边拾贝" scheme="https://thysrael.github.io/tags/%E6%B5%B7%E8%BE%B9%E6%8B%BE%E8%B4%9D/"/>
    
    <category term="直观总结" scheme="https://thysrael.github.io/tags/%E7%9B%B4%E8%A7%82%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>Sys4AI-Transformer</title>
    <link href="https://thysrael.github.io/posts/7dc4ea13/"/>
    <id>https://thysrael.github.io/posts/7dc4ea13/</id>
    <published>2025-01-30T14:24:19.000Z</published>
    <updated>2026-01-06T06:59:09.855Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>当我们提到大模型 LLM 的时候，总是和 Transformer 这种架构联系在一起，似乎只有使用了 Transformer 架构的深度学习模型才配叫作大模型。</p><p>不过以我目前浅薄的认知，我倒觉得 Transformer 并不是 LLM 的核心特征，因为 LLM 的算法变化很快，Transformer 从 2017 年到现在有了多种变体，也有完全不采用 Transformer 架构的 AI。我个人感觉 LLM 的核心有两点：</p><ul><li>模型参数极大：我们认为模型参数越多，模型就越智能。这是“涌现”的一种具体体现。</li><li>采用“预训练-微调-推理”范式：这种范式使得模型的通用性得到了增强，划分了不同的生态位。</li></ul><p>我希望在下文中记录一下关于 LLM 或者 Foundation Model 的基础知识，以避免被这个时代抛下太久。</p><hr><h2 id="二、数学基础"><a href="#二、数学基础" class="headerlink" title="二、数学基础"></a>二、数学基础</h2><h3 id="2-1-张量求导"><a href="#2-1-张量求导" class="headerlink" title="2.1 张量求导"></a>2.1 张量求导</h3><h4 id="2-1-1-规律"><a href="#2-1-1-规律" class="headerlink" title="2.1.1 规律"></a>2.1.1 规律</h4><p>之前我多次学习张量求导的数学定义，但是总感觉非常生硬和无厘头，因为我不清楚到底要求多少次偏导，导数矩阵的形状是什么（甚至有些都不是矩阵了，而是 3 维张量了），还有如何跟我之前学过的数学分析、线性代数知识联系在一起。</p><p>经过又一次的学习，我总结出如下规律：</p><ul><li>导数矩阵分量的个数，是因变量的分量个数与自变量的分量个数的乘积。这个细想下来非常显然，在求导的时候，当然应该对影响每个因变量的每个自变量求偏导，这样的每个结果就是导数矩阵中的一个分量。我们以最经典的雅各比矩阵举例，一个 $M$ 维的向量函数对于 $N$ 维的自变量向量求导，它的雅各比矩阵形状是 $M \times N$ ，也就是有 $MN$ 个分量。</li><li>导数矩阵的形状是出于适配链式求导法则等制定的，在梯度下降法中导数矩阵的形状需要与自变量形状相同。由上一条可知，我们已经可以确定导数矩阵中分量具体是什么了，但是如何排列这些分量组成导数矩阵依然不确定。经过我的学习，我觉得形状没有简单的规律可以总结。其核心在于一定要适用于链式法则，也就是要考虑到所有中间变量并求和（下文会有详述）。我又注意到，为了使梯度下降法生效，那么导数矩阵（也就是梯度矩阵），必须和自变量矩阵形状相同，要不然就无法实现矩阵减法了（对应元素相减）。顺便吐槽一下，梯度下降法并不是那么合理，用自变量减去导数，并没有实际意义，它只是在自变量处于极值点时，达到不动点。</li><li>在 ML 中，张量只是一种表示形式，高维度张量一定最终会被转化成矩阵运算。高维度导数张量是由于因变量是向量或者矩阵导致的，在 ML 中广泛存在向量值函数（比如 $softmax$）或者矩阵值函数（比如 $Attenction \space Score$）。但是不用担心，我们的最终目的是求解损失值函数对各个参数的导数矩阵，因为损失值函数是一个标量函数，所以导数矩阵一定是低维张量（后面有介绍）。</li></ul><h4 id="2-1-2-链式法则"><a href="#2-1-2-链式法则" class="headerlink" title="2.1.2 链式法则"></a>2.1.2 链式法则</h4><p>在标量世界中，对于 $z = g(y), y = f(x)$ ，链式法则通常写做：</p><script type="math/tex; mode=display">\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x}</script><p>那么如果 $x$ 不再是标量，而是一个向量 $X$ 怎么办，那么我们依然可以列出来 $X$ 的任意分量 $x_i$ ，有：</p><script type="math/tex; mode=display">\frac{\partial z}{\partial x_i} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x_i}</script><p>显然如果 $X$ 是一个矩阵，那么情形也是类似的，对于任意分量 $x_{ij}$ ，有：</p><script type="math/tex; mode=display">\frac{\partial z}{\partial x_{ij}} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x_{ij}}</script><p>上面的都很简单且显然，那么我们可以思考一下如果 $y$ 不再是标量，而是一个 $N$ 维向量 $Y$ 怎么办？那么 $X$ 的某个分量 $x_i$ 就可以通过影响 $Y$ 的所有分量 $y_1, y_2, \dots, y_n$ 来影响 $z$ ，所以当我们求 $z$ 对 $x_i$ 的偏导的时候，要考虑到所有的 $y_k$ ，所以其形式如下：</p><script type="math/tex; mode=display">\frac{\partial z}{\partial x_{i}} = \sum_{k = 1} ^ N \frac{\partial z}{\partial y_k} \frac{\partial y_k}{\partial x_{i}}</script><p>这个分量形式也可以被整理成更加规整的矩阵乘法形式（毕竟上面就是乘加运算），也就是如下所示：</p><script type="math/tex; mode=display">\frac{\partial z}{\partial \mathbf{X}} = \frac{\partial z}{\partial \mathbf{Y}} \frac{\partial Y}{\partial \mathbf{X}}\\= \begin{bmatrix}\frac{\partial z}{\partial y_1} \frac{\partial z}{\partial y_2} \cdots \frac{\partial z}{\partial y_n}\end{bmatrix}\begin{bmatrix}\frac{\partial y_1}{\partial x_{1}} & \frac{\partial y_1}{\partial x_{2}} & \cdots & \frac{\partial y_1}{\partial x_{m}} \\\frac{\partial y_2}{\partial x_{1}} & \frac{\partial y_2}{\partial x_{2}} & \cdots & \frac{\partial y_2}{\partial x_{m}} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_n}{\partial x_{1}} & \frac{\partial y_n}{\partial x_{2}} & \cdots & \frac{\partial y_n}{\partial x_{m}}\end{bmatrix}</script><p>右边的那个方阵，就是传说中的雅各比矩阵。</p><p>我们在“规律”这一节探讨的求导矩阵的形状问题，其实核心就在于求导矩阵的形状，可以在链式法则中直接应用，而不需要经过大量的 reshape。</p><p>那如果 $X$ 和 $Y$ 有任一方是一个矩阵怎么办？可以想见，此时的雅各比矩阵就不再是二维的了，而是三维张量或者四维张量了。此时就很难整理成矩阵乘法的形式了，但是分量求和公式依然成立。</p><p>而在 ML 实践中，常常因为有些 $\frac{\partial z}{\partial y_k}\frac{\partial y_k}{\partial x_i}$ 项为零，进而可以简化高维张量运算。</p><p>我们来举个例子，以 ML 中常见的全连接层 $Y = WX$ 为例（省略了偏置量 $B$），其中 $X$ 是 $N$ 维向量，$Y$ 是 $M$ 维向量，$W$ 是 $M \times N$ 维矩阵。那么在反向传播中，就有：</p><script type="math/tex; mode=display">\frac{\partial Y}{\partial W} = \begin{bmatrix}\begin{bmatrix}\frac{\partial y_1}{\partial w_{11}} & \frac{\partial y_1}{\partial w_{12}} & \cdots & \frac{\partial y_1}{\partial w_{1n}} \\\frac{\partial y_1}{\partial w_{21}} & \frac{\partial y_1}{\partial w_{22}} & \cdots & \frac{\partial y_1}{\partial w_{2n}} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_1}{\partial w_{m1}} & \frac{\partial y_1}{\partial w_{m2}} & \cdots & \frac{\partial y_1}{\partial w_{mn}}\end{bmatrix} \\\begin{bmatrix}\frac{\partial y_2}{\partial w_{11}} & \frac{\partial y_2}{\partial w_{12}} & \cdots & \frac{\partial y_2}{\partial w_{1n}} \\\frac{\partial y_2}{\partial w_{21}} & \frac{\partial y_2}{\partial w_{22}} & \cdots & \frac{\partial y_2}{\partial w_{2n}} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_2}{\partial w_{m1}} & \frac{\partial y_2}{\partial w_{m2}} & \cdots & \frac{\partial y_2}{\partial w_{mn}}\end{bmatrix}\\\vdots\\\begin{bmatrix}\frac{\partial y_n}{\partial w_{11}} & \frac{\partial y_n}{\partial w_{12}} & \cdots & \frac{\partial y_n}{\partial w_{1n}} \\\frac{\partial y_n}{\partial w_{21}} & \frac{\partial y_n}{\partial w_{22}} & \cdots & \frac{\partial y_n}{\partial w_{2n}} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_n}{\partial w_{m1}} & \frac{\partial y_n}{\partial w_{m2}} & \cdots & \frac{\partial y_n}{\partial w_{mn}}\end{bmatrix}\end{bmatrix}</script><p>这个式子看着就非常恐怖，再进行矩阵运算不得活活难死（其实还好），但是我们注意到对于任意分量 $y_i$ ，它等于：</p><script type="math/tex; mode=display">y_i = \sum_{j = 0}^N w_{ij} x_j</script><p>也就是说，对于特定的 $i$， $y_i$ 不和 $W$ 的所有分量有关，而是只跟 $W$ 第 $i$ 行分量有关，也就是：</p><script type="math/tex; mode=display">\frac{\partial Y}{\partial W} = \begin{bmatrix}\begin{bmatrix}\frac{\partial y_1}{\partial w_{11}} & \frac{\partial y_1}{\partial w_{12}} & \cdots & \frac{\partial y_1}{\partial w_{1n}} \\0 & 0 & \cdots & 0 \\\vdots & \vdots & \ddots & \vdots \\0 & 0 & \cdots & 0 \\\end{bmatrix} \\\begin{bmatrix}0 & 0 & \cdots & 0 \\\frac{\partial y_2}{\partial w_{21}} & \frac{\partial y_2}{\partial w_{22}} & \cdots & \frac{\partial y_2}{\partial w_{2n}} \\\vdots & \vdots & \ddots & \vdots \\0 & 0 & \cdots & 0 \\\end{bmatrix}\\\vdots\\\begin{bmatrix}0 & 0 & \cdots & 0 \\0 & 0 & \cdots & 0 \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_n}{\partial w_{m1}} & \frac{\partial y_n}{\partial w_{m2}} & \cdots & \frac{\partial y_n}{\partial w_{mn}}\end{bmatrix}\end{bmatrix}=\begin{bmatrix}\begin{bmatrix}x_1 & x_2 & \cdots & x_n \\0 & 0 & \cdots & 0 \\\vdots & \vdots & \ddots & \vdots \\0 & 0 & \cdots & 0 \\\end{bmatrix} \\\begin{bmatrix}0 & 0 & \cdots & 0 \\x_1 & x_2 & \cdots & x_n \\\vdots & \vdots & \ddots & \vdots \\0 & 0 & \cdots & 0 \\\end{bmatrix}\\\vdots\\\begin{bmatrix}0 & 0 & \cdots & 0 \\0 & 0 & \cdots & 0 \\\vdots & \vdots & \ddots & \vdots \\x_1 & x_2 & \cdots & x_n \\\end{bmatrix}\end{bmatrix}</script><p>其实我们都没有必要再关注这个复杂的 $\frac{\partial Y}{\partial W}$ 的稀疏性质了，我们直接回归本源，我们的核心目的是求解损失函数 $l$ 对 $W$ 的导数，那么按照原本来说，有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial w_{ij}} = \sum_{k = 0}^M \frac{\partial l}{\partial y_k} \frac{\partial y_k}{\partial w_{ij}}</script><p>又因为在 $i,j$ 确定的情况下， $w_{ij}$ 只会影响 $Y$ 的 $y_i$ 分量，所以上面这个式子就会变化成：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial w_{ij}} = \sum_{k = 0}^M \frac{\partial l}{\partial y_k} \frac{\partial y_k}{\partial w_{ij}} = \frac{\partial l}{\partial y_i}\frac{\partial y_i}{\partial w_{ij}} = \frac{\partial l}{\partial y_i}x_j</script><p>有了这样的化简后，就可以被整理成新的向量乘法，如下所示：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial W} = \frac{\partial l}{\partial Y} X^T</script><p>再次变得简洁优雅。</p><p>这件事情很启发我，我之前学习反向传播时，太关注复杂的神经网络的梯度的张量表示了，动不动就会出现三维或者四维的张量，然后陷入停滞。而实际上，就算在数学上产生了这些拦路虎，我们也并不在意，因为这些高维张量本来就不是我们的目的，它只是链式求和公式的一种形式。我们会重新回到链式求和公式，来构建更加简单的矩阵乘法，而不是固守高维张量。</p><h4 id="2-1-3-标量-张量"><a href="#2-1-3-标量-张量" class="headerlink" title="2.1.3 标量-张量"></a>2.1.3 标量-张量</h4><p>标量对张量进行求导时，生成的导数矩阵和张量（无论张量是标量、向量还是矩阵）的形状完全相同。比如说对于一个 $N$ 维列向量 $X$ ，其导数矩阵如下所示：</p><script type="math/tex; mode=display">\frac{\partial y}{\partial \mathbf{X}} = \begin{bmatrix}\frac{\partial y}{\partial x_1} \\\frac{\partial y}{\partial x_2} \\\vdots \\\frac{\partial y}{\partial x_n}\end{bmatrix}</script><p>而对于 $M \times N$ 维的矩阵求导，其形式也是类似的：</p><script type="math/tex; mode=display">\frac{\partial y}{\partial \mathbf{X}} = \begin{bmatrix}\frac{\partial y}{\partial x_{11}} & \frac{\partial y}{\partial x_{12}} & \cdots & \frac{\partial y}{\partial x_{1n}} \\\frac{\partial y}{\partial x_{21}} & \frac{\partial y}{\partial x_{22}} & \cdots & \frac{\partial y}{\partial x_{2n}} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y}{\partial x_{m1}} & \frac{\partial y}{\partial x_{m2}} & \cdots & \frac{\partial y}{\partial x_{mn}}\end{bmatrix}</script><h4 id="2-1-4-张量-张量"><a href="#2-1-4-张量-张量" class="headerlink" title="2.1.4 张量-张量"></a>2.1.4 张量-张量</h4><p>当因变量是张量的时候，就是对于因变量张量的每一个分量都应用一遍上文介绍的“标量-张量”方法。因变量分量会组成导数张量的外层维度，每个元素都是一个“标量-张量”导数矩阵。我们举个例子，有 $2 \times 3$ 维的向量 $X$ 和 $3 \times 2$ 维的 $Y$ 相乘得到 $2 \times 2$ 维的 $Z$ ，对于 $\frac{\partial Z}{\partial X}$ 有：</p><script type="math/tex; mode=display">\frac{\partial Z}{\partial X} =\begin{bmatrix}\frac{\partial z_{11}}{\partial X} & \frac{\partial z_{12}}{\partial X} \\\frac{\partial z_{21}}{\partial X} & \frac{\partial z_{22}}{\partial X} \\\end{bmatrix}=\begin{bmatrix}\begin{bmatrix}\frac{\partial z_{11}}{\partial x_{11}} & \frac{\partial z_{11}}{\partial x_{12}} & \frac{\partial z_{11}}{\partial x_{13}} \\\frac{\partial z_{11}}{\partial x_{21}} & \frac{\partial z_{11}}{\partial x_{22}} & \frac{\partial z_{11}}{\partial x_{23}} \\\end{bmatrix}& \begin{bmatrix}\frac{\partial z_{12}}{\partial x_{11}} & \frac{\partial z_{12}}{\partial x_{12}} & \frac{\partial z_{12}}{\partial x_{13}} \\\frac{\partial z_{12}}{\partial x_{21}} & \frac{\partial z_{21}}{\partial x_{22}} & \frac{\partial z_{21}}{\partial x_{23}} \\\end{bmatrix}\\\begin{bmatrix}\frac{\partial z_{21}}{\partial x_{11}} & \frac{\partial z_{21}}{\partial x_{12}} & \frac{\partial z_{21}}{\partial x_{13}} \\\frac{\partial z_{21}}{\partial x_{21}} & \frac{\partial z_{21}}{\partial x_{22}} & \frac{\partial z_{21}}{\partial x_{23}} \\\end{bmatrix}& \begin{bmatrix}\frac{\partial z_{22}}{\partial x_{11}} & \frac{\partial z_{22}}{\partial x_{12}} & \frac{\partial z_{22}}{\partial x_{13}} \\\frac{\partial z_{22}}{\partial x_{21}} & \frac{\partial z_{22}}{\partial x_{22}} & \frac{\partial z_{22}}{\partial x_{23}} \\\end{bmatrix}\end{bmatrix}</script><p>可以看到最后形成了四维 $2 \times 2 \times 2 \times 3$ 的导数矩阵。</p><p>如果我们考虑“向量-向量”这种特殊形式的求导，就会生成著名的雅可比矩阵（Jacobian Matrix）。考虑 $M$ 维 $Y$ 向量对 $N$ 维 $X$ 向量求导，有：</p><script type="math/tex; mode=display">J = \frac{\partial \mathbf{Y}}{\partial \mathbf{X}} =\begin{bmatrix}\frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \cdots & \frac{\partial y_1}{\partial x_n} \\\frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \cdots & \frac{\partial y_2}{\partial x_n} \\\vdots & \vdots & \ddots & \vdots \\\frac{\partial y_m}{\partial x_1} & \frac{\partial y_m}{\partial x_2} & \cdots & \frac{\partial y_m}{\partial x_n}\end{bmatrix}</script><p>这里列出的方阵，横轴是 $x$ 分量，而纵轴是 $y$ 分量。我个人觉得只要保证链式法则的基本要求，似乎转置一下也没有大关系，矩阵形状和矩阵乘法，只不过是一种简写的方式。</p><p>在 ML 中因为向量值函数很常见，所以经常可能会出现高维度张量，它们似乎就无法被擅长矩阵这种低维张量计算的 GPU 或者加速器中处理了，而实际上，正如“链式法则”这一章节中提到的，我们很少真正计算高维度张量。</p><h4 id="2-1-5-实例"><a href="#2-1-5-实例" class="headerlink" title="2.1.5 实例"></a>2.1.5 实例</h4><p>最后放一张 MLP 的图来总结一下反向传播过程中的链式求导和常见导数：</p><p><img src="/posts/7dc4ea13/1jt_OvqvfWkvuSUau4BPVWQ.png" alt="img"></p><h3 id="2-2-FLOPS"><a href="#2-2-FLOPS" class="headerlink" title="2.2 FLOPS"></a>2.2 FLOPS</h3><h4 id="2-2-1-GEMM"><a href="#2-2-1-GEMM" class="headerlink" title="2.2.1 GEMM"></a>2.2.1 GEMM</h4><p>GEMM 即 General Matrix Multiply ，就是最为常见的矩阵乘法操作。</p><p>对于一个 $M \times K$ 的矩阵与一个 $K \times N$ 的矩阵进行 GEMM 运算，FLOPS 是 $2 MNK$ 。</p><p>这是因为结果矩阵中有 $MN$ 个元素，而每个元素都是一个 $K$ 维行向量和一个 $K$ 维列向量的点积结果。而点积需要进行 $K$ 次乘法操作和 $K - 1$ 次加法操作，故总共需要约 $2K$ 次操作（其实我觉得这里存疑，因为如果是 MAC，Multi-Add 的话，其实点积只需要 $K$ 次操作）。进而 GEMM 需要 $2MNK$ 次操作。 </p><p>总之在 GEMM 中，FLOPS 分别是 3 个维度的一次函数。</p><h4 id="2-2-2-损失函数"><a href="#2-2-2-损失函数" class="headerlink" title="2.2.2 损失函数"></a>2.2.2 损失函数</h4><p>在神经网络中的最后一层，往往输出一个 $N$ 维向量 $Z$ ，我们需要根据向量 $Z$ 来计算损失函数 $l$ ，我们考虑一种最常见的损失函数：</p><script type="math/tex; mode=display">l = \sum^{N}_{i = 1} (z_i - t)^2</script><p>其中 $t$ 是目标期望值，那么就有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial Z} = 2(Z - T)</script><p>其中 $T$ 是一个每个分量均为 $t$ 的 $N$ 维向量。因为要进行 $N$ 次元素操作，此时的 FLOPS 就是 $N$ 。</p><h4 id="2-2-3-隐藏层"><a href="#2-2-3-隐藏层" class="headerlink" title="2.2.3 隐藏层"></a>2.2.3 隐藏层</h4><p>我们首先定义一下隐藏层，首先我们有一个 $N$ 维的输入向量 $I$ ，他会先经过线性变换变成一个 $M$ 维向量 $Y$ ，如下所示：</p><script type="math/tex; mode=display">Y = WI + B</script><p> 然后经过激活函数 $\sigma(Y)$ 的元素变化进行激活，有：</p><script type="math/tex; mode=display">\sigma(y_i) = \frac{1}{1 + e^{-x}}</script><p>我们记录 $M$ 维向量 $O$ 为激活后的值，即：</p><script type="math/tex; mode=display">O = \sigma(Y)</script><p>我们首先计算正向传播一个向量的 FLOPS。在计算 $Y$ 这个步骤的 FLOPS 是 $2NM$ ，计算 $O$ 这个步骤是 $M$ ，所以总体的 FLOPS 就是 $2NM + M$  （常数凑活看吧，领会精神）。</p><p>然后我们计算反向传播一个向量的 FLOPS。我们还需要定义一些其他辅助计算的符号。反向传播是遵循链式法则的，所以我们在计算当前层时，一定已经有了后面一个隐藏层输入的梯度，而后一个隐藏层的输入就是当前隐藏层的输出，也就是说，我们已知 $\frac{\partial l}{\partial O}$ 的值了。</p><p>在这个反向传播的过程中，我们希望求解参数的梯度 $\frac{\partial l}{\partial W}, \frac{\partial l}{\partial B}$ ，此外，我们还需要求解 $\frac{\partial l}{\partial I}$ ，虽然这个值和当前层的参数更新没有关系，但是上一层的反向传播的参数梯度，需要 $\frac{\partial l}{\partial I}$ ，正如我们需要  $\frac{\partial l}{\partial O}$ 一样。</p><p>首先我们计算激活值的梯度，有</p><script type="math/tex; mode=display">\frac{\partial l}{\partial Y} = \frac{\partial l}{\partial O} \frac{\partial O}{\partial Y}</script><p>又因为有：</p><script type="math/tex; mode=display">\sigma'(x) = \sigma(x) \cdot (1 - \sigma(x))</script><p>所以有：</p><script type="math/tex; mode=display">\frac{\partial O}{\partial Y} = \begin{bmatrix}o_1 (1 - o_1) \\o_2 (1 - o_2) \\\cdots \\o_m (1 - o_m)\end{bmatrix}</script><p>计算 $\frac{\partial l}{\partial Y}$ 的过程总 FLOPS 是 $2M$ ，先计算出 $\frac{\partial O}{\partial Y}$ 的 FLOPS 是 $M$ ，然后 $\frac{\partial l}{\partial O}$ 和 $\frac{\partial O}{\partial Y}$ 对应元素相乘，FLOPS 是 $M$ 。</p><p>然后我们计算权重矩阵的梯度，有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial W} = \frac{\partial l}{\partial Y} \frac{\partial Y}{\partial W}</script><p>按理说 $\frac{\partial Y}{\partial W}$ 是一个三维张量，比较难处理，但是又因为线性变换的特性（在“链式规则”处证明），有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial W} = \frac{\partial l}{\partial Y} I^T</script><p>因为 $\frac{\partial l}{\partial Y}$ 是 $M$ 维， $I$ 是 $N$ 维，所以总 FLOPS 是 $MN$ （没有加法过程，所以没有常数 $2$）。但是如果还要考虑用 $\frac{\partial l}{\partial W}$ 来修正 $W$ ，那么总 FLOPS 就是 $2MN$ 。</p><p>然后我们计算偏置量的梯度，有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial B} = \frac{\partial l}{\partial Y} \frac{\partial Y}{\partial B}</script><p>又因为：</p><script type="math/tex; mode=display">\frac{\partial Y}{\partial B} =\begin{bmatrix}1 \\1 \\\cdots \\1\end{bmatrix}</script><p>所以：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial B} = \frac{\partial l}{\partial Y}</script><p>FLOPS 直接可忽略，如果算上更新 $B$ ，那么 FLOPS 是 $N$ 。</p><p>最后我们还需要计算 $\frac{\partial l}{\partial I}$ ，有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial I} = \frac{\partial l}{\partial Y} \frac{\partial Y}{\partial I}</script><p>又因为：</p><script type="math/tex; mode=display">\frac{\partial Y}{\partial I} = W^T</script><p>所以总的 FLOPS 是一次 $M$ 维向量与 $M \times N$ 维矩阵乘法的 FLOPS，也就是 $2MN$ 。</p><p>所以总得来看，反向传播的 FLOPS 是 $4MN$ 左右，但是这个值很没有意义，只是说，它的量值依然是正比于输入维度 $N$ 和输出维度 $M$ 。</p><h4 id="2-2-4-Attention"><a href="#2-2-4-Attention" class="headerlink" title="2.2.4 Attention"></a>2.2.4 Attention</h4><p>Softmax 是一个独特的元素映射函数，这里记录一下它的梯度函数。设 softmax 的输入是一个 $N$ 维向量 $Z$ ，输出是一个 $N$ 维向量 $P$ ，有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial Z} = \frac{\partial l}{\partial P} \frac{\partial P}{\partial Z}</script><p>其中 $\frac{\partial P}{\partial Z}$ 是一个 $N \times N$ 维的雅克比矩阵。有如下定理，当 $i = j$ 时：</p><script type="math/tex; mode=display">\frac{\partial p_{i}}{\partial z_j} = p_i(1 - p_i)</script><p>当 $i \ne j$ 时：</p><script type="math/tex; mode=display">\frac{\partial p_{i}}{\partial z_j} = -p_ip_j</script><p>那么这里的反向传播的本质也是一个矩阵与向量乘法，FLOPS 大约是 $N^2$ 。</p><p>而 Attention 的其他部分用到了在隐藏层中没有出现过的矩阵乘法，比如说 $QK^T$ 计算，看似会产生四维张量，实际上和线性变换类似，非常直观，设：</p><script type="math/tex; mode=display">S = \frac{QK^T}{\sqrt{d_k}}</script><p>有：</p><script type="math/tex; mode=display">\frac{\partial l}{\partial Q} = \frac{\partial l}{\partial S} \frac{\partial S}{\partial Q} = \frac{1}{\sqrt{d_k}} \frac{\partial l}{\partial S} K \\\frac{\partial l}{\partial K} = \frac{\partial l}{\partial S} \frac{\partial S}{\partial K} = \frac{1}{\sqrt{d_k}} \frac{\partial l}{\partial S} Q</script><p>因此依然是矩阵与向量乘法的 FLOPS，FLOPS 是 $Q,K$ 维度的乘积，也就是 $seq_len \times d_{k}$ 。</p><h3 id="2-3-Im2Col"><a href="#2-3-Im2Col" class="headerlink" title="2.3 Im2Col"></a>2.3 Im2Col</h3><p>IM2Col 的意思是 Image To Column，本质是将卷积计算转换成矩阵乘法，然后因为矩阵乘法已经被优化得很好了，所以可以加速计算。如下所示：</p><p><img src="/posts/7dc4ea13/image-20250202175313184.png" alt="image-20250202175313184"></p><p>但是这种方式并不从理论上减少计算的复杂度，只是比较简单实现，并且效果较好。此外 FFT 也可以用于加速卷积计算，并且是理论上加速。</p><hr><h2 id="三、Transformer"><a href="#三、Transformer" class="headerlink" title="三、Transformer"></a>三、Transformer</h2><h3 id="3-1-Embedding"><a href="#3-1-Embedding" class="headerlink" title="3.1 Embedding"></a>3.1 Embedding</h3><p>我们都知道人工神经网络中每一层的神经网络都可以对前一层输入进行一次矩阵运算（如果刨除激活不算的话），从线性代数的知识可知，这其实是在做一次空间映射，如果矩阵是 $M \times N$ 的，那么每经过一层，就是将一个原本在 $M$ 维空间向量映射到一个 $N$ 维的空间中。</p><p>人工神经网络的原理是将一段数据先 tokenize ，也就是将原本的字符串之类（比如我们和 chatgpt 说的话）的东西转换成一组一维的向量，每个标量被称为一个 token ，然后将他们映射到一个向量空间中，这个过程叫做“嵌入”（embedding），然后就是对于这个向量的一次次映射。</p><p>那么我们这样做的直观理解是什么，我觉得是这样的，人工神经网络是在描述语义。说白了，就是通过构建一个语义空间的方式去掌握各个 token 的语义，语义空间就是一个多维向量空间。那么为什么一个多维向量空间就可以描述语义呢？因为多维向量空间中存在距离，我们可以用距离的方式来描述两个 token 的相似性，而这就构成了语义。比如在一个空间中，当我们观测到“苹果，梨，香蕉”的距离很近，那么可能就是因为她们都具有水果的语义。</p><p>语义空间的设计有两个极端，一个是一维标量，另一个是独热码。如果用一维标量的话，有些复杂的语义没有办法表示，比如说“苹果”，它既有“水果”的意思，又有“电子品牌”的意思，那么它应该既和“香蕉”离得近，又和“三星”离得近，但是“香蕉”和“三星”不应该离那么近。而独热码则是尽可能的扩大自己的维度，并只使用一个维度，那么我们很难表示出相近的含义，因为独热码的所有点的距离都是相同的。</p><p>Embedding 的维度通常被称为 $d_{model}$ 。</p><p>此外，为了将位置信息（Position），也就是当前 token 在序列中的位置考虑在内，我们还要经过一个位置编码（Position Encoding）的过程，说白了就是将位置编码进去，非常显然。</p><h3 id="3-2-Attention"><a href="#3-2-Attention" class="headerlink" title="3.2 Attention"></a>3.2 Attention</h3><p>Transformer 最初开发出来被用于进行机器翻译，其中最有特色的点就在于使用了 Attention 机制。为了理解 Attention 机制，有必要了解一下在 Transformer 提出之前，人们是怎样进行机器翻译的。</p><p>我最初的理解是，机器翻译就是存着一个字典，然后一个词一个词的翻译就够了（也就是只进行依次 embedding 和逆向 embedding 的过程）。但是仔细一想就不太可能，这是因为不同语言之间并不是只需要逐词翻译，因为语法的不同，导致不同语言的上下文顺序也是不同的。所以人们最先设计出的机器翻译机制，是让每个句子对应一个向量，被称作 Context Vector，在翻译的时候，先用神经网络将句子编码成 Context Vector，然后再用神经网络解码成另一门语言的句子，如下图所示：</p><p><img src="/posts/7dc4ea13/image-20250202211138372.png" alt="image-20250202211138372"></p><p>至于为什么要使用 RNN，将 token 一个个喂入神经网络，而不是一股脑将整个句子当作输入一口气喂进去。是因为翻译的难点在于理解上下文，RNN 可以更好的发现序列之间的关系。</p><p>但是这种方式也存在缺点，那就是 context vector 的维度是有限的，而句子的量级显然不是 context vector 所能容纳的，所以这种方法的效果并不好，而如果我们减小句子的范围，那么就又不利于长上下文的理解。此外，这个方法的并行度也非常差，是串行输入每一个 token 。</p><p>Attention 机制就是解决这个问题的。它的最本质思想是，上下文关系如果存储在隐藏层或者 context vector 中，很容易受到维度的限制，那么我们就专门用一个方阵 $A$ ，用 $a_{ij}$ 记录第 $i$ 个 token 和第 $j$ 个 token 之间的联系，根据这个方阵再结合每个 token 的语义，来确定输出。</p><p>那么 Attention 具体是怎样的呢？首先我们需要先介绍 Attention 的输入。经过 Embedding 过程，每个 token 都是一个 $1 \times d_{model}$ 维的行向量。</p><p>我们设序列长度为 $t$ ，那么输入可以被整理成一个 $t \times d_{model}$ 的矩阵 $X$。</p><p>那么我们如何获得 $A$ 呢（学名叫作 $Attention \space Score$）？很简单，我们可以用向量内积的思想，如果两个向量的内积很大，就说明两个向量离得很近，因为如果二者夹角很小的话，那么内积就会增大。虽然这并不严谨，但是这基本上就是它的思想了。于是我们有了：</p><script type="math/tex; mode=display">A = XX^T</script><p>也就是说有：</p><script type="math/tex; mode=display">a_{ij} = x_i x_j^T</script><p>所以 $a_{ij}$ 就可以表示第 $i$ 个 token 和第 $j$ 个 token 之间的相似度。$A$ 是一个 $t \times t$ 的方阵。</p><p>当然在有了 $A$ 并不够，我们只是获得了不同 token 之间的联系，但是我们并没有考虑原本 token 的语义，所以我们再将 $A$ 与 $X$ 相乘，那么就可以得到一个新的 $t \times d_{model}$ 的矩阵 $Y$ ，如下所示：</p><script type="math/tex; mode=display">Y = AX = XX^TX</script><p>我们将 $Y$ 视为多个 token 的集合，那么对于第 $i$ 个 token，也就是第 $i$ 行的行向量，有：</p><script type="math/tex; mode=display">Y_i = \sum^t_{j = 0} a_{ij} X_j</script><p>现在让我们重新回顾这个模型，我们的输入是一个含有 $t$ 个 token 的集合 $X$ ，输出依然是含有 $t$ 个 token 的集合 $Y$ 。此时 $Y$ 中的每个 token，都是 $X$ 中所有 token 的语义的加权和，权重是 $X$ 中对应的 token 与其他剩余 token 的相关性。在 $X$ 中每个 token 的语义都是独立的（每个 token 单独进入网络层），经过 Attention 机制后，具有相似语义的 token 会互相影响，此时 $Y$ 中的每个 token 都是携带上下文信息的。</p><p>下面举个例子，有 $d_{model} = 4, t = 6$ ，如下所示： </p><p><img src="/posts/7dc4ea13/attention1.drawio.png" alt=""></p><p>这里我有一个有趣的思考，就是并不是所有的模型都可以随着规模增大而性能更好。比如说 RNN 相比于传统的多层感知机，就可以有更多的层数，这是因为 RNN 削弱因层数增多而导致的“梯度消失”现象。但是正如前所述，虽然 RNN 避免了“梯度消失”，但是过于串行化的算法和较低的状态维度（我觉得这点可能可以改进），导致我们无法进一步扩大模型规模。而基于 Attention 机制的 Transformer 模型则有更好的可拓展性，并行化程度高，所以才能在更大规模时有更加智能的表现。</p><h3 id="3-3-Cross-Attention"><a href="#3-3-Cross-Attention" class="headerlink" title="3.3 Cross-Attention"></a>3.3 Cross-Attention</h3><p>上文介绍的 Attention 机制和”Attention is All you Need“这篇论文中的并不太一样，这是因为我在上面只是介绍了最为基础的 Attention 原理，在下文中我会进一步拓展这个机制。</p><p>首先我们注意到，上文中计算 $Y$ 的公式，只有一个输入 $X$ ，如下所示：</p><script type="math/tex; mode=display">Y = XX^TX</script><p>那么这里面的 $X$ 的含义都一样吗？其实并不应该一样，还是以机器翻译来举例，如果输入只有一个 $X$ ，那么谈什么翻译呢？如果希望将中文翻译成英语，怎么也得有 3 个输入，也就是：</p><ul><li>待翻译的中文</li><li>中译英字典（语料库）的索引</li><li>中译英字典（语料库）的内容</li></ul><p>Attention 机制是可以满足这 3 种输入的，正好 $Y$ 的表达式中有 3 个 $X$ ，他们可以被差异化成如下公式：</p><script type="math/tex; mode=display">Y = AV = QK^TV</script><p>此时各个字符的含义如下：</p><ul><li><p>$Q$：Query，即要翻译的中文语句，它的形状是 $t_{sen} \times d_{in}$ 。</p><ul><li>$t_{sen}$ 被理解成语句的长度。</li><li>$d_{in}$ 是表示一个中文 token 语义所需的分量个数。</li></ul></li><li><p>$K$：Key，即中译英字典（语料库）的索引，它的形状是 $t_{all} \times d_{in}$ 。</p><ul><li>$t_{all}$ 可以被理解成所有中文语料的个数。</li></ul></li><li><p>$A$：Attention Score，依然是相关性分数，它的形状是 $t_{sen} \times t_{all}$ 。</p><ul><li>分量 $a_{ij}$ 表示待翻译的中文语句中的第 $i$ 个 token 和语料库中的第 $j$ 个语料的相关性。这很合理，我们查字典的过程，不就是根据待翻译的中文语句中的字，来查询对应的英文吗？</li><li>Attention 对应的就是”查字典“这个过程，只不过”查字典“可以查到准确的单词，而在复杂的翻译过程中，只能查询到与 token 语义相近的语料。</li></ul></li><li><p>$V$：Value，即中译英字典（语料库）的内容，它的形状是 $t_{all} \times d_{out}$ 。</p><ul><li>$d_{out}$ 是表示一个英文 token 语义所需的分量个数。</li></ul></li><li><p>$Y$：Output，即翻译好的英文语句，它的形状是 $t_{sen} \times d_{out}$ 。</p><ul><li>它的每个行向量都是一个英文的 token。</li><li>它是 $A$ 和 $V$ 的乘积，也就是每个英文的 token，都是中译英语料库中以相关性为权重形成的加权和。</li></ul></li></ul><p>上面这个中译英的例子可能还不是那么直观，具体的例子很难举，因为语言和数字的对应还是有些难度的。我们举另一个例子，我们进行一个”成绩-能力“的翻译。也就是我们有上一届同学的考试成绩，还有他们的学习能力，我们希望根据当前这届同学的成绩，来推测他们的学习能力是怎样的，完成一个从”成绩“到”能力“的翻译。</p><p>考试一共有”数学“和”语文“ 2 个科目，成绩是 5 分制。学习能力一共有”记忆“、”创新“和”勤奋“ 3 种。数据都是我瞎编的，勿杠。</p><p>当前这届同学的成绩就构成了 $K$ 矩阵，其中：</p><ul><li>$t_{sen}$ 为 2，表示这届两名同学 s1 和 s2</li><li>$d_{in}$ 为 2，表示 2 门考试科目。</li></ul><p>$K$ 如下所示：</p><div class="table-container"><table><thead><tr><th></th><th>数学</th><th>语文</th></tr></thead><tbody><tr><td>s1</td><td>5</td><td>2</td></tr><tr><td>s2</td><td>1</td><td>5</td></tr></tbody></table></div><p>往届同学的成绩构成了 $Q$ 矩阵，其中 $t_{all}$ 为 3，表示前一届的 3 名同学 s3, s4 和 s5 。$Q$ 如下所示：</p><div class="table-container"><table><thead><tr><th></th><th>数学</th><th>语文</th></tr></thead><tbody><tr><td>s3</td><td>2</td><td>5</td></tr><tr><td>s4</td><td>4</td><td>1</td></tr><tr><td>s5</td><td>3</td><td>3</td></tr></tbody></table></div><p>又因为 $A = QK^T$ ，如下所示：</p><div class="table-container"><table><thead><tr><th></th><th>s3</th><th>s4</th><th>s5</th></tr></thead><tbody><tr><td>s1</td><td>20</td><td>22</td><td>21</td></tr><tr><td>s2</td><td>27</td><td>9</td><td>20</td></tr></tbody></table></div><p> 可以看到非常合理，s1 擅长数学而不擅长语文，s4 也是如此，所以在 3 名往届学生中，s4 和 s1 最像，相关性也是最高的（22）。s2 擅长语文而不擅长数学，与 s3 最为相似，可以看到相关性也很高（27）。</p><p>往届同学的能力构成了 $V$ 矩阵，其中 $d_{out}$ 为 3，对应 3 种能力，如下所示：</p><div class="table-container"><table><thead><tr><th></th><th>记忆</th><th>创新</th><th>勤劳</th></tr></thead><tbody><tr><td>s3</td><td>15</td><td>6</td><td>7</td></tr><tr><td>s4</td><td>3</td><td>12</td><td>5</td></tr><tr><td>s5</td><td>9</td><td>9</td><td>6</td></tr></tbody></table></div><p>在我编的这个情景下，”记忆“越好，”语文“成绩就越高；”创新“越好，”数学“成绩就越高；”勤劳“越好，总成绩就越高。可以看到基本上都是合理的，比如 s3 擅长语文，它的”记忆“能力就比”创新“能力好。</p><p>然后我们用 $Y = AV$ 来看看当前这届同学的能力，有：</p><div class="table-container"><table><thead><tr><th></th><th>记忆</th><th>创新</th><th>勤劳</th></tr></thead><tbody><tr><td>s1</td><td>555</td><td>573</td><td>376</td></tr><tr><td>s2</td><td>612</td><td>450</td><td>354</td></tr></tbody></table></div><p>可以看到基本上还是合理的（当然我们也不能指望着只有 3 个数据的数据集有多准确）。s1 的数学成绩很高而语文成绩很差，按理说他的“创新”能力应该是高于“记忆”能力的；s2 的语文成绩很高而数学成绩很差，按理说他的“记忆”能力是高于“创新”能力的。这些推理都被 $Y$ 体现了。</p><p>当然 $Y$ 也存在两个问题：</p><ul><li>能力绝对值过大了，在 $V$ 矩阵中能力值都是两位数，而在 $Y$ 中都是 3 位数，很夸张。</li><li>能力相对值不明显，以 s1 同学为例，明明“数学”成绩比“语文”成绩高 3 分，但是“记忆”能力和“创新”能力却相差不大。</li></ul><p>第一个问题主要是因为 $A$ 矩阵没有按行归一化导致的，按理说加权和里的权重应该是一个“百分比”，而我们没有归一化，所以绝对值会偏大。而第二个问题是因为在数据集中，只有 s4 一个同学和 s1 一样是“数学比语文高”，而且 s4 还不如 s1 成绩好，所以 s1 的能力很容易被 s3 和 s5 的数据干扰，如果有办法让与 s1 更相似的同学（也就是 s4）相比于不相似的同学更突出。</p><p>上述两个问题都可以使用 $softmax$ 来改善，这是一个作用于向量的向量函数，如下所示：</p><script type="math/tex; mode=display">\sigma(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{n} e^{z_j}}</script><p>可以看到这是一个归一化函数，所以解决了第一个问题。而 $softmax$ 中使用的指数函数，使得相关性高的分量变得更加明显，所以解决了第二个问题。</p><p>我们用 $softmax$ 来修正 $A$ ，$softmax$ 对矩阵作用，本质就是对矩阵中的每一个行向量作用，修正后的 $A$ 如下所示：</p><div class="table-container"><table><thead><tr><th></th><th>s3</th><th>s4</th><th>s5</th></tr></thead><tbody><tr><td>s1</td><td>0.09</td><td>0.67</td><td>0.24</td></tr><tr><td>s2</td><td>0.99</td><td>0</td><td>0</td></tr></tbody></table></div><p>可以看到这时的相关性非常完美，按照修正后的 $A$ 计算修正后的 $Y$：</p><div class="table-container"><table><thead><tr><th></th><th>记忆</th><th>创新</th><th>勤劳</th></tr></thead><tbody><tr><td>s1</td><td>5.5</td><td>10.7</td><td>5.4</td></tr><tr><td>s2</td><td>15.0</td><td>6.0</td><td>7.0</td></tr></tbody></table></div><p>可以看到非常合理。</p><p>此外，$A$ 还有一个问题，就是当 $d_{in}$ 过大时， $QK^T$ 很容易产生数值很大的分量。</p><p>所以在实践上，我们需要对每个分量除以 $\sqrt{d_{in}}$ 来避免数据的溢出，这个过程被称为 scale。</p><p>综上所述，我们得出了一个和最终版本非常像的算子，如下所示：</p><script type="math/tex; mode=display">Y = softmax(\frac{QK^T}{\sqrt{d_{in}}})V</script><p>当 $Q, K, V$ 来源不相同时，我们称之为 Cross-Attention，即交叉注意力机制，常用于机器翻译。而当 $Q, K, V$ 来源相同时，则被称为 Self-Attention 机制，常用于发现上下文联系，理解或者产生新的语义。</p><p>我们看一下 Transformer 的架构图：</p><p><img src="/posts/7dc4ea13/image-20250204155327984.png" alt="image-20250204155327984"></p><p>我们以“中译英”来距离， <code>inputs</code> 就是要中译英的语料库，<code>outputs</code> 刚开始就是要翻译的中文 ，<code>output probabilities</code> 是翻译好的英文（之所以叫作 probalities，应该是因为这里采用了 Self-Regression 架构）。在右上角的橙色 Attention 块中，<code>inputs</code> 负责提供 $K, V$ ，而 <code>outputs</code> 提供 $Q$。</p><p>那么如果在 Self-Attention 中，还有必要区分 $Q, K, V$ 吗？还是说只要像最开始那样，直接使用 $X$ 就好了呢？其实还是有必要区分 $Q, K, V$ 的，在上面的介绍中可以看出， $Q, K, V$ 是各司其职，所以即使在 Self-Attention 中，也是有这样的分工。我们可以使用三个权重矩阵，来使得来源相同的 $Q, K, V$ 有不同的作用，如下所示：</p><script type="math/tex; mode=display">Q = X W_Q \\K = X W_K \\V = X W_V</script><p>这样做，还可以改变 $Q, K, V$ 的形状，他们不再必须和 $X$ 保持相同的形状 $t \times d_{model}$ ，而是可以变成 $t \times d_k$ 和  $t \times d_v$ 。之所以没有 $d_q$ ，是因为 $d_q$ 和 $d_k$ 是相等的。</p><h3 id="3-4-Self-Regression"><a href="#3-4-Self-Regression" class="headerlink" title="3.4 Self-Regression"></a>3.4 Self-Regression</h3><p>正如前文所述，Attention 机制最初用于机器翻译，所以其核心部分是 Cross-Attention。而如今大火的生成式（Generative）大模型，则对原始模型的一个改进。也就是“自回归”（Self-Regression）。</p><p>自回归的意思是，将输出重新作为输入，用于产生新的输出，周而复始。这个概念还比较好理解，问题在于它和 Attention 机制并不搭配，如下所示：</p><script type="math/tex; mode=display">Y = softmax(\frac{QK^T}{\sqrt{d_{in}}})V</script><p>最后生成的是一个 $t \times d_{v}$ 的矩阵 $Y$ ，在机器翻译中，这就是那个翻译好的英文句子。但是在生成式中呢？难道就是把 prompt 翻译了吗？显然不是的，实际上 self-regression 的设计非常“浪费”，它只会选取 $Y$ 的最后一个行向量，作为生成出来的 token 输出，并将这个 token 连接 $X$ 的最后面，其伪代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">generative</span><span class="token punctuation">(</span>prompt<span class="token punctuation">)</span><span class="token punctuation">:</span>    X <span class="token operator">=</span> prompt             <span class="token comment"># prompt 是最开始的输入</span>    R <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>                 <span class="token comment"># Result 最开始为空</span>        <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>        Y <span class="token operator">=</span> attention<span class="token punctuation">(</span>X<span class="token punctuation">)</span>   <span class="token comment"># 进行 attention 机制</span>        y <span class="token operator">=</span> Y<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span>         <span class="token comment"># 取最后一个 token</span>        <span class="token keyword">if</span> y <span class="token operator">==</span> <span class="token string">'&lt;EOS&gt;'</span><span class="token punctuation">:</span>   <span class="token comment"># 如果是 End of Sequence，则退出循环</span>            <span class="token keyword">break</span>        R <span class="token operator">+=</span> y             <span class="token comment"># 记录 token 作为输出</span>        X <span class="token operator">+=</span> y             <span class="token comment"># 将 token 作为输入</span>        <span class="token keyword">return</span> R<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这种“浪费”会被下文的 KV-Cache 缓解。</p><h3 id="3-5-Encoder-Decoder"><a href="#3-5-Encoder-Decoder" class="headerlink" title="3.5 Encoder Decoder"></a>3.5 Encoder Decoder</h3><p>我们从上面的 Transformer 架构中注意到，上面一共有 3 种 Attention，我们详细介绍过的是位于右上角的 cross attention 块。在左下和右下还有两个 self-attention 快，分别是 encode-attention 和 decode-attention。</p><p>这两种 attention 实现的都不是翻译任务，而是一种“让 token 关注上下文语义”的任务。通过 attention，原本独立的 token 语义会在上下文的影响下发生变化，这对之后的 cross-attention 是一种帮助。</p><p>但是为什么 decoder-attention 相比于 encode-attention，多了一个 Sequence Mask 机制，它说得是对于 $A$ 矩阵中的 $a_{ij}$ ，如果有 $j &gt; i$ ，那么就会被 $softmax$ 忽略。这样的目的是确保模型在生成当前词时，只能使用当前词及其之前的词，而不能“偷看”未来的词。而 encoder 就没有这个问题，为了获得语料库中的所有语义，Encoder 是允许使用全部上下文的。</p><p>这里有个问题，就是在 Self-Regression 中，本来就是逐词生成的，在当前词没有生成的时候，未来的词也肯定没有生成，那么就算模型想偷看，也偷看不成。为了解释清楚，我们就需要理解逐词生成是推理的行为，而在训练的时候，模型是并行处理整个目标序列的，所以才需要掩码。</p><h3 id="3-5-Multi-Head"><a href="#3-5-Multi-Head" class="headerlink" title="3.5 Multi-Head"></a>3.5 Multi-Head</h3><p>除了计算资源的浪费之外，我们还注意到 Self-Attention 的并行度又变差了。本来在机器翻译中，Attention 机制改进了 RNN 每个 token 逐个翻译的缺点，可以并行生成一整个输出语句（也就是 token 集合 $Y$）。但是在这个算法中，并行生成的 $Y$ 只用最后一个 token 的行向量 $y$ ，就又变成串行生成了。</p><p>但是这种“串行生成”是 self-regression 的精髓，所以我个人感觉很难改变。但是我们依然有办法加速，那就是每次 attention 的时候通过减少 <script type="math/tex">d_k, d_v</script> 来提高速度。这种缩减 <script type="math/tex">d_k, d_v</script> 的行为，可以理解为在原本 <script type="math/tex">d_{model}</script> 的空间里提取特征。</p><p>那么仅提供一个特征，就容易导致精度丧失，所以我们可以使用多个 attention，提取多个不同的特征，最后再将结果拼接在一起，这样提高了并行度和延迟，又不损失精度。我们设模型的 head 数为 $h$ ，通常有：</p><script type="math/tex; mode=display">d_{model} =  h \cdot d_k</script><p>我们举一个例子，有参数：</p><ul><li>$d_{model} = 4$</li><li>$t = 6$</li><li>$h = 2$</li><li>$d_k = 2$</li></ul><p>示意图如下：</p><p><img src="/posts/7dc4ea13/attention2.drawio.png" alt=""></p><h3 id="3-6-KV-Cache"><a href="#3-6-KV-Cache" class="headerlink" title="3.6 KV Cache"></a>3.6 KV Cache</h3><p>正如前所述，在 Self-Regression 中我们计算 $Y$ 的目的只是为了得到最后一个行向量 $Y_t$ ，在上述计算中，有很多计算是冗余的，我们在上图中用“实心”标出计算 $Y_t$ 所需要的分量：</p><p><img src="/posts/7dc4ea13/attention3.drawio.png" alt=""></p><p>也就是说，只需要 $Q$ 的最后一个行向量，全部的 $K, V$ 向量，就可以满足计算要求，我们并不需要全部的 $Q$ 。</p><p>更进一步，因为 $K, V$ 都是逐步增长的，也就是每次增加最后一个横向量，所以前面的部分都是可以被 cache 的，避免了 $X$ 每次都需要与 $W$ 进行运算，如果对 $K,V$ 进行 cache（用“交叉线”填充），那么计算量会进一步减少：</p><p><img src="/posts/7dc4ea13/attention4.drawio.png" alt=""></p><p>但是因为 $KV$ 的形状都包括一个 $t$ ，所以在长上下文场景下（也就是 $t$ 很大），会导致缓存的数据很多，这样就会导致 GPU 的访存压力很大。</p><h3 id="3-7-Prefill-amp-Decode"><a href="#3-7-Prefill-amp-Decode" class="headerlink" title="3.7 Prefill &amp; Decode"></a>3.7 Prefill &amp; Decode</h3><p>在了解完 KV Cache 后，我们可以来计算一下 Attention 机制的复杂度。对于一个 decode 阶段的 token 的时间复杂度而言：</p><p>使用 $W_Q$ 对输入进行投影得到 $q$ ，时间复杂度是：</p><script type="math/tex; mode=display">2 d_{model}d_k</script><p>利用 $q$ 与 $K$ 相乘得到注意力得分，时间复杂度是：</p><script type="math/tex; mode=display">2d_{k}t</script><p>对于一个 $t$ 个元素的注意力得分与 $V$ 进行计算，时间复杂度是：</p><script type="math/tex; mode=display">2d_{k}t</script><p>也就是说，这是一个与 $t$ 呈线性时间复杂度的算法。</p><p>不过这么好的方法并不适用于整个算法。在我们生成第一个 token 的时候，是将整个 prompt 进行计算的，所以还是会需要计算出一个注意力方阵，而非一个注意力向量。</p><p>那么在 prefill 阶段，那么注意力得分的时间复杂度是：</p><script type="math/tex; mode=display">2d_kt^2</script><p>而与 $V$ 进行计算的时间复杂度为：</p><script type="math/tex; mode=display">2d_kt^2</script><p>都是 $O(t^2)$ 的算法。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;当我们提到大模型 LLM 的时候，总是和 Transformer 这种架构联系在一起，似乎只有使用了 Transformer 架构的深度学习模型才配叫作大模型。&lt;/p&gt;
&lt;p&gt;不过以我目前浅薄的认知，我倒觉得 Transformer 并不是 LLM 的核心特征，因为 LLM 的算法变化很快，Transformer 从 2017 年到现在有了多种变体，也有完全不采用 Transformer 架构的 AI。我个人感觉 LLM 的核心有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型参数极大：我们认为模型参数越多，模型就越智能。这是“涌现”的一种具体体现。&lt;/li&gt;
&lt;li&gt;采用“预训练-微调-推理”范式：这种范式使得模型的通用性得到了增强，划分了不同的生态位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我希望在下文中记录一下关于 LLM 或者 Foundation Model 的基础知识，以避免被这个时代抛下太久。&lt;/p&gt;</summary>
    
    
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/categories/Sys4AI/"/>
    
    
    <category term="直观理解" scheme="https://thysrael.github.io/tags/%E7%9B%B4%E8%A7%82%E7%90%86%E8%A7%A3/"/>
    
    <category term="Sys4AI" scheme="https://thysrael.github.io/tags/Sys4AI/"/>
    
    <category term="S9假期" scheme="https://thysrael.github.io/tags/S9%E5%81%87%E6%9C%9F/"/>
    
  </entry>
  
  <entry>
    <title>计算机系统-分布式</title>
    <link href="https://thysrael.github.io/posts/47da48a7/"/>
    <id>https://thysrael.github.io/posts/47da48a7/</id>
    <published>2025-01-16T06:49:42.000Z</published>
    <updated>2025-08-15T12:16:49.026Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、分布式系统"><a href="#一、分布式系统" class="headerlink" title="一、分布式系统"></a>一、分布式系统</h2><h3 id="1-1-总论"><a href="#1-1-总论" class="headerlink" title="1.1 总论"></a>1.1 总论</h3><p>分布式系统指的是利用多个单体计算机系统构建的多体计算机系统。</p><p>与并行计算不同，分布式系统更侧重于用多个计算实体完成<strong>非常多个</strong>细碎的任务。而并行计算侧重于利用多个计算实体来更快更高效地完成<strong>一个</strong>计算任务。</p><p>分布式系统包括了执行流的分布式和数据的分布式。在北航 OO 课电梯问题中，涉及了多线程，属于是执行流的分布式，这就导致我以为分布式只是执行流的分布式，涉及的问题只是互斥和同步，这是非常片面的。数据的分布式指的是，存在多个数据副本，cache 就是一种数据分布式的实体。</p><h3 id="1-2-背景"><a href="#1-2-背景" class="headerlink" title="1.2 背景"></a>1.2 背景</h3><p>分布式系统虽然出现在各个系统层次，比如说多核的 CPU，多级存储系统，多线程的程序，分布式式的 Web APP。但是大部分的分布式理论都源于 Web APP，这是因为 Web 时代发展的红利导致的。</p><p>传统的 Web APP 架构被称为 LAMP，即 Linux + Apache + MySQL + PHP。</p><p>LAMP 的拓展性并不符合要求，其核心原因在于 Web APP 的用户拓展性非常好，一个用户生产 1M 的数据（非常小），那么一百万个用户就会生产出 1T 的数据，这就不再是一个磁盘可以简单存放的了，我们就需要一个存储的集群了。及时人们可以制造出一个非常大的磁盘，但是它的速度一定是会受到影响的。</p><p>计算也是同理，可能一个用户请求的计算量并不大，但是一百万个用户的计算量就非常大了。而受到摩尔定律和登纳德缩放定律失效的影响，人们是无法制造出一个超级强悍的单体芯片的，这就导致必须使用多个芯片。</p><p>这个时候将系统从单机拓展成分布式系统，就非常自然了。</p><h3 id="1-3-组成"><a href="#1-3-组成" class="headerlink" title="1.3 组成"></a>1.3 组成</h3><p>下面会介绍一个分布式的 Web APP （电商平台）由哪几个部分组成，其架构如图所示：</p><p><img src="/posts/47da48a7/image-20250116163924056.png" alt="image-20250116163924056" style="zoom:40%;"></p><p>存在如下分布式：</p><ul><li>外存的分布式：将数据存放在多个外存服务器上，也就是分布式的文件系统和分布式的数据库。</li><li>内存的分布式：分布式外存无法高效的缓存数据，且 APP 所在的单机的内存不够大，所以将内存也进拓展，也就是 Memcached</li><li>app 的分布式：app 不需要部署在同一个单机上，可以部署在不同单机上，每个单机负责不同的功能。</li><li>Load Balance：仅存在一个 app 依然非常局限，可以在多个单机上启动多个相同的 app，再由 Load Balancer 来决定请求发往哪个更为空闲的单机。这种技术除了依赖负载平衡技术外，还依赖 HTTP 协议的无状态性，使得负载均衡可以做得很轻松，如果是有状态的，那么负载均衡调度的就不再只是请求，就还包括之前的状态信息。</li><li>CDN：将一些多媒体资源，放置在距离用户更近的地方。</li><li>Compute 的分布式：将大量的计算任务放在单独的集群上（这张图上好像没有）。</li></ul><h3 id="1-4-CAP-理论"><a href="#1-4-CAP-理论" class="headerlink" title="1.4 CAP 理论"></a>1.4 CAP 理论</h3><h4 id="1-4-1-介绍"><a href="#1-4-1-介绍" class="headerlink" title="1.4.1 介绍"></a>1.4.1 介绍</h4><p>CAP 指的是分布式的三种重要属性，之所以将它们三个单独拎出来，是因为这三个属性构成了一个不可能三角（我也不知道有没有严格数学证明）。</p><p>我个人认为理解 CAP 理论的难点在于理解清楚这三个属性的定义，他们的定义如下：</p><ul><li><strong>C</strong>onsistency（一致性）：多个数据备份中的数据是一模一样的，用户可能会对某个备份中的数据进行修改，然后另一个数据备份中的数据也会跟随变化，保持一致。当系统没有一致性的时候，可能存在不同的数据备份，也就是正确性出现了问题。</li><li><strong>A</strong>vailable（可用性）：这是非常难理解的一个属性，他指的是用户对于数据的操作（读取或者写入一个数据等）是成功的概率非常高。而当失去可用性的时候，用户的操作经常会收到“操作失败，请重新尝试”的回应。</li><li><strong>P</strong>artition Tolerance（分区容忍性）：这个也非常难理解，它指的是在多个数据备份失去连接的时候（也就是所谓的“分区”），系统依然保持一致性和可用性的能力。C 和 A 对于一个单机系统而言，是非常自然的属性（要不然就是写出来 bug）了，但是如果是分布式系统，一旦数据备份之间失去同步能力，那么是很难保证 C 和 A 的。</li></ul><p>那么为什么这是一个不可能三角呢？我们考虑如下图这样的情况，目前有两个分布式服务器 $S_1$、$S_2$，这两个服务器里都存储着数据 $V$ 的数据备份，客户 $C$ 可以读写 $V$ ，在开始阶段，$S_1$、$S_2$ 中 $V$ 的值都是 $v_0$ 。</p><p>然后出现了网络故障，导致 $S_1$、$S_2$ 之间的同步机制失效，出现了分区情况。然后 $C$ 向 $S_1$ 写入 $V$ 的值为 $v_1$ ，因为缺乏同步机制，所以 $S_2$ 中 $V$ 的值依然是 $v_0$ 。</p><p>此时如果 $C$ 从 $S_2$ 处读取 $V$ 的值，那么有两种可能，第一种是为了可用性，我们告诉 $C$ ，$V$ 的值是 $v_0$ ，这样就牺牲了一致性原则。第二种是我们为了一致性，告诉 $C$ 目前网络出现问题，读取并不成功，那么这样就牺牲了可用性原则。</p><p><img src="/posts/47da48a7/image-20250117111613066.png" alt="image-20250117111613066" style="zoom:40%;"></p><p>从这个例子就可以看出，CAP 不可兼得。而根据业务场景的不同，我们选择牺牲的属性也不同：</p><ul><li>CA 牺牲 P：牺牲 P 就意味着系统不再是一个真正的分布式系统了，因为分布式系统出现“分区“是非常核心的场景，或者说，在分布式系统中不考虑”同步机制“，就过于理想和不可靠了。所以牺牲 P 的系统往往是一个单机系统（或者是某种无法享受分布式优势的分布式系统）。对于一个单机系统而言，保证 C 和 A 是非常基本的事情。</li><li>CP 牺牲 A：牺牲 A 意味着操作有可能失败，但是只要操作成功了，那么它的一致性（也就是正确性）就得到了保证。对于银行 APP 或者支付宝这种对于正确性比较看重的软件，一般会采用这种策略。毕竟谁也不希望自己的账户里的钱突然没了。</li><li>AP 牺牲 C：牺牲 C 意味着虽然每次操作都会得到响应，但是操作的结果不一定保证一致性。对于微信对这种实时性要求比较高的聊天软件，一般会采用这种策略。这也是我们在使用微信的时候，常常会出现”引用的内容不存在“的原因。</li></ul><h4 id="1-4-2-ACID-vs-BASE"><a href="#1-4-2-ACID-vs-BASE" class="headerlink" title="1.4.2 ACID vs BASE"></a>1.4.2 ACID vs BASE</h4><p>在传统的数据库理论研究中，有 ACID 理论：</p><ul><li><strong>原子性 (Atomicity)</strong>：原子性保证了事务中的所有操作要么全部成功，要么全部失败，不能只完成部分操作。如同“原子”一样，事务是不可分割的。</li><li><strong>一致性 (Consistency)</strong>：一致性保证了事务在执行前后，数据库的数据必须是合法的。当事务完成后，所有数据约束条件都必须得到满足。</li><li><strong>隔离性 (Isolation)</strong>：隔离性确保了并发执行的事务之间不会互相干扰。每个事务的执行都像是独占资源，其他事务不能看到其未提交的结果。</li><li><strong>持久性 (Durability)</strong>：持久性确保了已提交的事务所做的更改是永久性的，即使系统崩溃或出现故障，这些更改也不会丢失。</li></ul><p>可以看出大部分的属性都是在保证 C 和 A，而对于 P 是没有描述的。</p><p>而随着 Web 时代的来临，传统的 ACID 就跟不上时代了，所以人们又提出了 BASE 理论：</p><ul><li><strong>基本可用性 (Basically Available)</strong>：系统在大多数时间内是可用的，即使在部分节点出现故障时也能处理请求。这意味着要牺牲部分一致性来保证系统的可用性。</li><li><strong>柔性状态 (Soft state)</strong>：在 BASE 理论中，数据的状态不是瞬时的一致性，而是在某个时间点上可能处于“柔性”的状态，允许数据在一定时间内有暂时的不一致性。</li><li><strong>最终一致性 (Eventual consistency)</strong>：在较长的时间内，系统会最终达到一致状态。这意味着虽然可能存在短期的不一致性，但经过一段时间后，所有副本最终会一致。</li></ul><p>从这里可以看出，BASE 的思路是弱化 C 和 A ，来保证 P。理论的变化折射出不同的时代需求。</p><h3 id="1-5-指标"><a href="#1-5-指标" class="headerlink" title="1.5 指标"></a>1.5 指标</h3><p>虽然 CAP 理论提出了一些关于分布式理论的指标，但是我觉得它更像是为了理论服务的，而不是为了实际的生产。</p><p>在实际中，我们更看重这些指标：</p><p><img src="/posts/47da48a7/image-20250117115544649.png" alt="image-20250117115544649" style="zoom:35%;"></p><p>我们为了这些指标发明了很多技术：</p><p><img src="/posts/47da48a7/image-20250117115658051.png" alt="image-20250117115658051" style="zoom:35%;"></p><p>和 CAP 理论一样，这些指标之间也存在一定的权衡，不同的技术满足不同的场景：</p><p><img src="/posts/47da48a7/image-20250117115923738.png" alt="image-20250117115923738" style="zoom:50%;"></p><p>我们下面会详细介绍一些特性的实现。</p><hr><h2 id="二、一致性"><a href="#二、一致性" class="headerlink" title="二、一致性"></a>二、一致性</h2><h3 id="2-1-总论"><a href="#2-1-总论" class="headerlink" title="2.1 总论"></a>2.1 总论</h3><p>在分布式系统中，会存在多个数据备份和多个执行流。这种情况下，很容易出现各个实体间操作和数据不一致的情况。也就是说，不一致是自然的，我们希望通过我们的努力，让这个复杂的模型简单下来。</p><p>我们简化模型的方式是这样的：</p><ul><li>所有数据只有一份拷贝</li><li>整体并发的操作序列会被等价成一个串行序列</li></ul><p>也就是如下图所示：</p><p><img src="/posts/47da48a7/image-20250117152505550.png" alt="image-20250117152505550" style="zoom:33%;"></p><p>那这是怎么办到的呢？从图上可以看出，确实很多操作就是同时进行的，而不是串行执行的呀？我们改变操作的顺序，本质是在改变操作的起始时间，如果两个操作重叠在一起了，那么我们就让一个操作晚一些启动，也就是<strong>阻塞</strong>一个操作，来将两个操作错开。</p><p>也就是说，为了编程的易用性，我们牺牲的是系统的性能，更具体一些，牺牲的是操作的时延（可能还有些别的）。</p><p>一致性模型的光谱（Spectrum）如下：</p><p><img src="/posts/47da48a7/image-20250117153143453.png" alt="image-20250117153143453" style="zoom:30%;"></p><p>我们还可以从另一个角度去分析为什么性能和易用性可以形成 tradeoff，当我们有一个并发的操作序列的时候，我们可以将他们排列成多种串行的全排列。一致性模型的本质，就是规定一些全排列是符合要求的，而另一些全排列是不符合要求的。越严格的一致性模型，允许的全排列的数目就越少，那么行为就更好被预测，那么易用性就高；而越宽松的模型，允许的全排列的数目就越多，那么底层实现就可以越灵活，性能就可以越好。</p><p>此外还需要强调，这里的一致性，并不完全等价于正确性，并不是说，我们只要遵循了某个一致性的模型，那么程序就不会出我们意想不到的 bug 了（不同的一致性模型可能有不同强度的编程约束，在这个约束下可能有一些奇怪的行为，但是都是符合一致性模型的内在逻辑的）。这是因为这里说的一致性里的操作，每个操作只涉及一个数据 object，而涉及多个 object 的操作（比如说银行账户的转账，涉及一个账户金额的减少和另一个账户金额的增加），就不再是一致性的范畴了，而是下文讨论的隔离性的范畴了。这种涉及多个 object 的操作被称作一个事务（Transaction，TX）。</p><h3 id="2-2-一致性模型"><a href="#2-2-一致性模型" class="headerlink" title="2.2 一致性模型"></a>2.2 一致性模型</h3><h4 id="2-2-1-Strict"><a href="#2-2-1-Strict" class="headerlink" title="2.2.1 Strict"></a>2.2.1 Strict</h4><p>strict 模型指的是按照每个 operation 的起始时间来对这些 operation 进行排列。那么这种模型基本上就等同于只允许一个全排列（因为每个 operation 的开始时间一般都是不同的），是最严格的模型。</p><p>这种模型基本上只是只能存在于理论中，因为在分布式系统中很难建立起一个全局的统一时钟。</p><h4 id="2-2-2-Linearizability"><a href="#2-2-2-Linearizability" class="headerlink" title="2.2.2 Linearizability"></a>2.2.2 Linearizability</h4><p>Linearizability 指的是如果 op1 的终止时间在 op2 的起始时间之前，那么在串行排列中，也一定是 op1 在 op2 之前。如下图所示，这种排列就是不被允许的：</p><p><img src="/posts/47da48a7/image-20250117163653516.png" alt="image-20250117163653516" style="zoom:40%;"></p><p>Linearizability 看似也是需要全局时钟的，而实际上并不是，Linearizability 的本质是确定各个操作的相对顺序而不是绝对顺序，所以实现难度会低一些。</p><h4 id="2-2-3-Sequential"><a href="#2-2-3-Sequential" class="headerlink" title="2.2.3 Sequential"></a>2.2.3 Sequential</h4><p>Sequential 指的是在每个计算实体上维护操作的相对顺序，而 Linearizability 是全局的相对顺序。</p><p>也就是说，下面的这个图（就是上面的那个图），虽然不符合 Linearizability ，但是符合 Sequential 。这是因为原本在 Linearizability 中存在的依赖，因为分别属于 P0 和 P1，所以在 Sequential 中并不存在：</p><p><img src="/posts/47da48a7/image-20250117164908037.png" alt="image-20250117164908037" style="zoom:40%;"></p><h4 id="2-2-4-Eventual"><a href="#2-2-4-Eventual" class="headerlink" title="2.2.4 Eventual"></a>2.2.4 Eventual</h4><p>Eventual 是一种弱一致性模型，它只能保证最终每个数据副本的状态是一致的，而中途的状态就不一定了。</p><p>这种模型似乎就是微信这种实时聊天软件所采用的模型，所以微信的实时性更好，但是经常出现错误。</p><h3 id="2-3-实现"><a href="#2-3-实现" class="headerlink" title="2.3 实现"></a>2.3 实现</h3><p>Linearizability 有一个特性，就是如果每个 object 的 Linearizability  得到了维持，那么整个系统的 Linearizability  就得到了维持。这个特性我其实不是太理解，因为我感觉不同的 object 之间也会存在一些依赖关系。所以就算了，只是记录在这里。</p><p>下面我们开始介绍 Linearizability 的实现，其中最简单的一个模型就是 Primary-Backup Model，也就是将某个数据备份设置为 Primary，而其他的数据备份是 Backup。所有的写入操作都需要先对所有的 Backup 写入后，再对 Primary 进行写入。所有的读取操作，也只是对于 Primary 的读取，并不会读 Backup，也就是如下：</p><p><img src="/posts/47da48a7/image-20250117170621899.png" alt="image-20250117170621899" style="zoom:33%;"></p><p>这种方式有两个很有趣的点，一个是要先写 backup，然后写 Primary，这是因为如果先写 Primary，那么就又可能在写操作还没有 Done 的时候，另一个读操作就可以读出来新值了，这就违反了 Linearizability 的定义。换句话说，将 Primary 的写入视为整个写入操作的结束，是一种实现 Linearizability 的一种手段。</p><p>另一个就是即使本地有数据，也依然要到 Primary 中去读取，这是因为 Read 操作必须完全在 Write 操作之前或者之后，下图就表示了一种不遵循这种方式造成的问题，Read_1 读出了新值，而 Read_2 明明在 Read_1 之后，读出的却是旧值。</p><p><img src="/posts/47da48a7/image-20250117171434398.png" alt="image-20250117171434398" style="zoom:33%;"></p><p>当然这种非常愚蠢的读操作也是有办法缓解的，我们还是能读取本地值的，即使本地值是 backup，也就是我们设计每个 object 有两种模式，一种模式下只可以读取本地值，而另一个模式不允许，必须读取 Primary 的值，当 Primary 要写 object 的时候，就会把模式调整为不允许，而写入完成后，就调整成允许。这种设计非常类似于 Cache Coherency 的设计。</p><p>此外 Backup 的 op 的顺序也可能因为网络等问题，导致和 Primary 上的 op 的顺序存在差异，如下所示：</p><p><img src="/posts/47da48a7/image-20250117172212189.png" alt="image-20250117172212189" style="zoom:33%;"></p><p>明明在 P0 上是 op1，op2，到了 P1 上就变成了 op2，op1 。我们可以给操作一个 seq number，来解决这个问题：</p><p><img src="/posts/47da48a7/image-20250117172401101.png" alt="image-20250117172401101" style="zoom:33%;"></p><p>在 Primary-Backup 模型中，Primary 因为承担了过多的通信开销，导致其成为性能瓶颈。我们可以采用分区的方法，也就是不同的 object 的 Primary 并不是同一个，来避免瓶颈问题，下图中 x 的 Primary 是 P0，而 y 的 Primary 是 P1：</p><p><img src="/posts/47da48a7/image-20250117172756191.png" alt="image-20250117172756191" style="zoom:33%;"></p><p>在了解完 Linearizability 的实现后，如果我们每次都只读取 local 的数据，写入的时候也只写入 local 数据，对于其他的备份，采用后台写入的方式，那么我们就得到了 Eventual 模型。</p><hr><h2 id="三、隔离性"><a href="#三、隔离性" class="headerlink" title="三、隔离性"></a>三、隔离性</h2><h3 id="3-1-总论"><a href="#3-1-总论" class="headerlink" title="3.1 总论"></a>3.1 总论</h3><p>在一致性这一章，我们讨论了在分布式系统下，多个并发 op 的一致性模型，但是即使我们让这些 op 满足了某种一致性，也是依然会出现问题的。比如说银行转账问题。出现问题的原因就在于，这些 op 是一个个零散得进行排序的，而在生产中，我们希望一组 op 可以绑定在一起进行排序（起码看上去是绑定在一起的），这样绑定在一起的一组 operation 我们就称之为“事务”（Transaction，TX）。</p><p>隔离性（Isolation）就是描述事务的性质，它指的是事务之间看上去是相互隔离的，不会出现“犬牙交错”的情况，也就是不同 TX 里的 op 交替执行。隔离性还有很多其他的名字，比如说 Before-After Atomicity，Serializability 等，都是相同的意思。在下文中我们主要采用 Serializability 这个词。</p><p>那么 Serializability 和 Consistency 的关系是什么？我觉得它们是不完全正交的。虽然他们看上去都涉及了某种“视图”，但是其本质是不一样的。Consistency 的视图是一个 op 的串行序列，而 Serializability 的视图是多个 op 的串行序列，也就是常说的 Grid Notion，在 Grid Notion 中有 3 个概念：</p><p>Operation ⊂ Transaction ⊂ Schedule</p><p>也就是一个调度（Schedule）就是一个“历史记录”，这段历史记录里有所有事务（Transaction）里的所有 Operation 的排列顺序。数据库开发者能做得就是“歪曲” Schedule 来获得性能，而用户需要明确被开发者歪曲过的 Schedule 和一个理想的 Schedule （一般就是原子性事务形成的 Schedule）之间的差距有多大。</p><p>如下所示：</p><p><img src="/posts/47da48a7/clipboard-20241105T194227.png" alt="clipboard-20241105T194227" style="zoom: 50%;"></p><p>写到这里其实我迷茫了，感觉虽然上面是两个 op 序列，但是实际上依然是一个 op 序列（或者说实际上依然是并行的，只是在视图上变成了串行的）。所以我也说不好。</p><p>Serializability 只是要求 TX 看上去保持原子性，其中的 op 是不能被打散的。但是在实际上如果真的这样去做，那么性能就又会受到一定的影响。所以实际上不同 TX 的 op 依然是交错执行，甚至是并行执行的，只是看上去是满足 Serializability 的，这点和 Consistency 类似，也是存在着多种 Serializability 的模型，如下所示：</p><p><img src="/posts/47da48a7/image-20250118173609510.png" alt="image-20250118173609510" style="zoom:33%;"></p><p>我在网上找到了一个图，因为图上有一些我不懂的概念，所以我不知道是不是可以说明 Serializability 和 Consistency 的不完全正交性。</p><p><img src="/posts/47da48a7/clipboard-20241105T194446.png" alt="clipboard-20241105T194446"></p><h3 id="3-2-Serializability-模型"><a href="#3-2-Serializability-模型" class="headerlink" title="3.2 Serializability 模型"></a>3.2 Serializability 模型</h3><h4 id="3-2-1-Conflict-Serializability"><a href="#3-2-1-Conflict-Serializability" class="headerlink" title="3.2.1 Conflict Serializability"></a>3.2.1 Conflict Serializability</h4><p>Conflict Serializability 是一种约束较强的 Serializability 模型。它首先定义了什么是 operation 之间的 conflict：</p><ul><li>它们操作同一个 object</li><li>至少其中一个是写操作</li><li>它们属于不同的事务</li></ul><p>然后如果一个 Schedule 的 conflict order 和某个串行化 Schedule 的 conflict order 是一样的，那么这个 Schedule 就是符合 Conflict Serializability 。</p><p>这个定义说得非常的不清楚，其实应该是这样。所谓的“串行化 Schedule”指的是让这些 TX（注意不是 op）串行化的 schedule。我们在现实中希望能交替或者并行执行不同 TX 中的 op，如果两个 op 毫不相干，那么显然是可以交替执行的。而 conflict 定义了一种 op 之间“相干”的关系，存在 conflict 的 op 就不能随便调度了，它必须和一种串行化的调度的 conflict 顺序相同（相当于和理想状态一致）。</p><p>而在实践中，我们可以用 Conflict Graph 是否成环来判断一个 schedule 是否满足 Conflict Serializability。conflict graph 是有向图，的节点是 TX（注意不是 op），而如果两个 TX 中有 op 是 conflict，那么就有一条边，从在 schedule 中更靠前的 op 所在的 TX 的节点，指向更靠后的 op 所在的 TX 的节点。当 Conflict Graph 成环的时候，就说明这个 schedule 是不满足 Conflict Serializability 的。</p><p>为什么 Conflict Graph 是否成环就可以判断是否是 Conflict Serializability 呢？这是因为对于一个串行的 schedule 而言，它的 Conflict Graph 一定是不成环的，这是非常显然的，反之，当一个 schedule 对应的 Conflict Graph 是成环的，那么它一定没有办法表示成一个串行的 schedule。所以我们就可以用 Conflict Graph 来判断。</p><h4 id="3-2-2-View-Serializability"><a href="#3-2-2-View-Serializability" class="headerlink" title="3.2.2 View Serializability"></a>3.2.2 View Serializability</h4><p>Conflict Serializability 的约束还是过强了，类似于 CPU 指令的调度，RAW，WAW，WAR 都会被判定为 conflict，而实际上约束并不需要这么强。比如说下图的 schedule 不符合 Conflict Serializability，但是读出的值、最后的状态（也就是我们在调度中最关心的两点），和串行 schedule “T1 -&gt; T2 -&gt; T3”一样：</p><p><img src="/posts/47da48a7/image-20250118202954360.png" alt="image-20250118202954360" style="zoom:40%;"></p><p>为了允许这种情况，我们发明了 View Serializability。它的定义就是，如果读操作和最终状态都和一个串行 schedule 相同，那么就是符合 View Serializability 的。也就是 View 的含义，即“看上去没啥毛病”。</p><p>View Serializability 就是一种较为理想的状态，它一共有 3 点要求（非常数学形式化的要求），其中第二点就是 RAW，而剩下两点是关于 init 和 end 的约束，就比 Conflict Serializability 要更加合理地多。</p><p>可惜的是，View Serializability 很难检验是否达成，而且也很难实现；相反的，Conflict Serializability 可以用 Conflict Graph 来检验，而且可以用 2PL 来实现。</p><h3 id="3-3-实现"><a href="#3-3-实现" class="headerlink" title="3.3 实现"></a>3.3 实现</h3><h4 id="3-3-1-2PL"><a href="#3-3-1-2PL" class="headerlink" title="3.3.1 2PL"></a>3.3.1 2PL</h4><p>Lock 是一种保证多个 Serializability 的有效手段。而具体而言，需要使用 2PL （2 Phase Lock）的方式。</p><p>按理说是一份 object 对应一份锁，但是并非锁的粒度仅仅取决于资源的粒度。比如说我们希望将 A 账户里的钱转账到 B 账户上，在语义上，A 和 B 是一个共同体，所以锁不能仅仅是 A Lock 和 B Lock（这样会导致有一个中间态是 A 账户的钱已经没了，而 B 账户还没有收到钱），而应该是一个 AB Lock 。</p><p>在实践上，我们没有必要真的声明一个 AB Lock，这样的话，一个有 N 个账户的银行就需要 $C_{n}^{2}$ 个锁了，这显然是不现实的。我们只需要同时拿住 A Lock 和 B Lock 即可保证粒度是 AB 。</p><p>从上面我们说到，锁的粒度不仅取决于资源本身的粒度，还取决于事务（上面的例子是转账）的粒度。如果一个事务需要多种资源，那么它应该同时拿多把锁。问题在于，拿锁这个行为并不能同时发生。两阶段锁（Two Phase Locking, 2PL）这个理论告诉我们，只要按照它说得做，就可以达到跟“同时拿两个锁”一样的效果。</p><p>2PL 的两个阶段：</p><ul><li>Expanding phase：只拿锁，不放锁</li><li>Shrinking phase：只放锁，不拿锁</li></ul><p>也就是说，以对共享资源进行具体的操作为界，在操作前只拿锁，在操作后只放锁，不会存在放锁后再拿锁的情况。</p><p><img src="/posts/47da48a7/image-20250118204019137.png" alt="image-20250118204019137" style="zoom:33%;"></p><p>Lock 有一个问题 Deadlock，为了避免死锁，其实 2PL 已经给出了一个方法，就是在拿锁的时候需要按照一定的顺序，而放锁的时候按照相反的顺序，这样就可以避免死锁。</p><p>但是这种方法的难点在于，很难给所有的共享的 object 进行一个排序，而且对程序员也是负担。</p><p>此外，我们还可以用 Conflict Graph 来判断是否出现死锁，但是这种检验方式成本也太大。所以我们现在一般采用一些启发式方法。</p><h4 id="3-3-2-OCC"><a href="#3-3-2-OCC" class="headerlink" title="3.3.2 OCC"></a>3.3.2 OCC</h4><p>OCC 即 Optimistic Concurrency Control。</p><p>相对应的，Lock 的方式被视为 Pessimistic 。这是因为拿锁的目的是为了保证 object 的独占性，但是这建立于这个 object 真的会被多个 TX 并发访问的假设，这个假设在某些场景下有些过于悲观了。</p><p>OCC 的意思是 TX 不再使用锁，而是直接操作，如果出现了竞态，那么再修正。即：</p><ul><li><p><strong>阶段 1：并发本地处理</strong></p><ul><li><p>读取数据到读集。</p></li><li><p>将写操作缓存在写集中。</p></li></ul></li><li><p><strong>阶段 2：验证可串行化（在临界区内）</strong></p><ul><li>验证是否保证可串行化：</li><li>检查读集中的数据是否被修改过。</li></ul></li><li><p><strong>阶段 3：提交或中止（在临界区内）</strong></p><ul><li><p><strong>中止</strong>：如果验证失败，则中止事务。</p></li><li><p><strong>提交</strong>：如果验证成功，则安装写集并提交事务。</p></li></ul></li></ul><p>Git 就是一种 OCC，每个人都在自己本地进行修改，而如果发生冲突，再手动 merge。</p><p>后两个阶段还是需要用锁来保证独占性的，但是因为这两个阶段都很短，所以性能开销并不大。后两个阶段伪代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">validate_and_commit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment"># phase 2 &amp; 3 with before-or-after</span>    <span class="token keyword">for</span> d <span class="token keyword">in</span> <span class="token builtin">sorted</span><span class="token punctuation">(</span>write_set<span class="token punctuation">)</span><span class="token punctuation">:</span>        d<span class="token punctuation">.</span>lock<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">for</span> d <span class="token keyword">in</span> read_set<span class="token punctuation">:</span>        <span class="token keyword">if</span> d has changed <span class="token keyword">or</span> d has been locked<span class="token punctuation">:</span>           abort<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">for</span> d <span class="token keyword">in</span> write_set<span class="token punctuation">:</span>        write<span class="token punctuation">(</span>d<span class="token punctuation">)</span>    <span class="token comment"># release the locks</span>    <span class="token comment"># ...</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>OCC 的问题在于经常会出现假阳性的 abort。所以当并发事务增多的时候，OCC 的性能并不好，其与 2PL 的对比如下：</p><p><img src="/posts/47da48a7/image-20250118211510893.png" alt="image-20250118211510893" style="zoom:50%;"></p><h4 id="3-3-3-HTM"><a href="#3-3-3-HTM" class="headerlink" title="3.3.3 HTM"></a>3.3.3 HTM</h4><p>HTM 即 Hardware Transactional Memory。从本质上讲，HTM 就是 OCC 思想的硬件实现。</p><p>Intel 在 Haswell 处理器上首次支持了了 HTM，被称为 Restricted Transactional Memory（RTM）。RTM 引入了 3 条新指令</p><pre class="line-numbers language-assembly" data-language="assembly"><code class="language-assembly">xbegin() ; 标识事务开始 xend() ; 标识事务开始 xabort() ; 标识事务中断<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>HTM 在实现上，使用 CPU Cache 来追踪 read/write_set，那如何探测 conflict 呢？是基于 Cache Coherency 实现的。</p><p>HTM 的问题在于支持的事务不能特别长，read/write_set 也不能特别大。</p><h4 id="3-3-4-MVCC"><a href="#3-3-4-MVCC" class="headerlink" title="3.3.4 MVCC"></a>3.3.4 MVCC</h4><p>OCC 在多并发读的性能不好，2PL 也是同样的，那么有没有什么方式可以优化多读少写的场景呢？</p><p>MVCC 即 multi-versioning concurrency control ，就是一种适应这种场景的新式版本控制方式。</p><p>其设计思路是维护 object 的多个 version，也就是多个 snapshot，用 time 作为版本号，这样可以避免并行读的竞争。</p><p><img src="/posts/47da48a7/image-20250118220106083.png" alt="image-20250118220106083" style="zoom:40%;"></p><p>其伪代码形式如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">Commit</span><span class="token punctuation">(</span>tx<span class="token punctuation">)</span><span class="token punctuation">:</span>   <span class="token keyword">for</span> record <span class="token keyword">in</span> tx<span class="token punctuation">.</span>write_set<span class="token punctuation">:</span>        lock<span class="token punctuation">(</span>record<span class="token punctuation">)</span>   let commit_ts <span class="token operator">=</span> FAA<span class="token punctuation">(</span>global_counter<span class="token punctuation">)</span>   <span class="token keyword">for</span> record <span class="token keyword">in</span> tx<span class="token punctuation">.</span>write_set<span class="token punctuation">:</span>       record<span class="token punctuation">.</span>insert_new_version<span class="token punctuation">(</span>commit_ts<span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span>        unlock<span class="token punctuation">(</span>record<span class="token punctuation">)</span> <span class="token keyword">def</span> <span class="token function">Get</span><span class="token punctuation">(</span>tx<span class="token punctuation">,</span> record<span class="token punctuation">)</span><span class="token punctuation">:</span>   <span class="token keyword">while</span> record<span class="token punctuation">.</span>is_locked<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>         <span class="token keyword">pass</span>   <span class="token keyword">for</span> version<span class="token punctuation">,</span>value <span class="token keyword">in</span> record<span class="token punctuation">.</span>sort_version_in_decreasing<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>       <span class="token keyword">if</span> version <span class="token operator">&lt;=</span> tx<span class="token punctuation">.</span>start_time<span class="token punctuation">:</span>           <span class="token keyword">return</span> value <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>其中 <code>commit</code> 是事务最终提交前的工作（主要是写操作），<code>get</code> 是读操作。</p><hr><h2 id="四、容错性"><a href="#四、容错性" class="headerlink" title="四、容错性"></a>四、容错性</h2><h3 id="4-1-RSM"><a href="#4-1-RSM" class="headerlink" title="4.1 RSM"></a>4.1 RSM</h3><p>增加容错最常用的方法，就是增加数据备份。这样即使出现网络错误，或者有什么物理 crash，备份的数据也保存完好。</p><p>但是让多个数据备份保持一致性，非常的困难。这里说的一致性，并不是上面的一致性，上面的一致性更侧重于从并发控制流角度的一致性，而这里的一致性更侧重于数据的一致性。</p><p>RSM 即 Replicated State Machine。它是人们提出的一种理论模型，这个理论指导了人们如何实现多个具有一致性的数据备份服务器。它将备份服务器视为状态机，只要初始状态一致，操作的顺序一致，那么最终状态就一定是一致的。这个理论将“让备份服务器保持一致”这个问题转换成了“让所有备份服务器的操作顺序一致”。</p><p>也就是说，我们需要确定一个唯一的执行序列（如果操作中有随机函数，还需要将执行结果记录下来），这样就能保证多个备份服务器的内容都一致了。</p><h3 id="4-2-Primary-Backup"><a href="#4-2-Primary-Backup" class="headerlink" title="4.2 Primary-Backup"></a>4.2 Primary-Backup</h3><p>很容易想到可以使用 Primary-Backup 机制来实现 RSM，也就是 Primary 负责接受用户请求，确定执行序列，然后发送给 Backup 让它执行。其形式如下：</p><p><img src="/posts/47da48a7/image-20250118222048627.png" alt="image-20250118222048627" style="zoom:33%;"></p><p>当 S1 挂掉了，那么 S2 就可以担任起备份的重任：</p><p><img src="/posts/47da48a7/image-20250118222217330.png" alt="image-20250118222217330" style="zoom:33%;"></p><p>这里图中只画了一个 Coordinator 用于将 clients 的请求转发给 Primary，而在实际的生产中，可能出现多个 Coordinator，这样可以更好地转发多个 client 的请求。此时就有可能出现问题了，就是一旦发生网络分区（Network Partition），如果两个  Coordinator  可能会选出两个 primary，如下所示：</p><p><img src="/posts/47da48a7/image-20250118222721272.png" alt="image-20250118222721272" style="zoom:33%;"></p><p>那这样每个 Primary 收到的请求就不一样了，不一致性自然就产生了。</p><p>所以我们提出了 View Server，说白了就是将“选出 Primary”这个任务指派给一个全局单例服务器上</p><p><img src="/posts/47da48a7/image-20250118222955428.png" alt="image-20250118222955428" style="zoom:33%;"></p><p>不过这种方法其实治标不治本，如果 VS 挂了怎么办？就没有办法解决了。只是说 VS 挂掉的可能性很小。</p><p>这种思路其实代表一种“中心化”的思路，即 Primary 是被某个中心权威（这个例子中是 VS）指定的。而当中心权威出现故障的时候，就无能为力了。</p><h3 id="4-3-Raft-共识算法"><a href="#4-3-Raft-共识算法" class="headerlink" title="4.3 Raft 共识算法"></a>4.3 Raft 共识算法</h3><p>共识算法是一种“分布式选举 Primary”的算法，这就避免了中心权威出现问题，进而导致系统不一致的情况出现。经典的共识算法有 Paxos 和 Raft。</p><p>Raft 于 2013 年由 Diego Ongaro 和 John Ousterhout 提出，旨在提供一种易于理解和实现的一致性协议。Raft 似乎是一个非常易懂的方式（比另一种 Paxos “希腊选举”），因为它论文里直接写伪代码了（面向工程师而非数学家）。不过似乎在设计上它比 Paxos 更反直觉。</p><p>具体的实现细节就不在这里写了。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、分布式系统&quot;&gt;&lt;a href=&quot;#一、分布式系统&quot; class=&quot;headerlink&quot; title=&quot;一、分布式系统&quot;&gt;&lt;/a&gt;一、分布式系统&lt;/h2&gt;&lt;h3 id=&quot;1-1-总论&quot;&gt;&lt;a href=&quot;#1-1-总论&quot; class=&quot;headerlink&quot; title=&quot;1.1 总论&quot;&gt;&lt;/a&gt;1.1 总论&lt;/h3&gt;&lt;p&gt;分布式系统指的是利用多个单体计算机系统构建的多体计算机系统。&lt;/p&gt;
&lt;p&gt;与并行计算不同，分布式系统更侧重于用多个计算实体完成&lt;strong&gt;非常多个&lt;/strong&gt;细碎的任务。而并行计算侧重于利用多个计算实体来更快更高效地完成&lt;strong&gt;一个&lt;/strong&gt;计算任务。&lt;/p&gt;
&lt;p&gt;分布式系统包括了执行流的分布式和数据的分布式。在北航 OO 课电梯问题中，涉及了多线程，属于是执行流的分布式，这就导致我以为分布式只是执行流的分布式，涉及的问题只是互斥和同步，这是非常片面的。数据的分布式指的是，存在多个数据副本，cache 就是一种数据分布式的实体。&lt;/p&gt;</summary>
    
    
    
    <category term="计算机系统" scheme="https://thysrael.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="计算机系统" scheme="https://thysrael.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="S9复习" scheme="https://thysrael.github.io/tags/S9%E5%A4%8D%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>计算机系统-虚拟化</title>
    <link href="https://thysrael.github.io/posts/670dae13/"/>
    <id>https://thysrael.github.io/posts/670dae13/</id>
    <published>2025-01-02T09:48:54.000Z</published>
    <updated>2025-08-15T12:16:49.046Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、CPU-虚拟化"><a href="#一、CPU-虚拟化" class="headerlink" title="一、CPU 虚拟化"></a>一、CPU 虚拟化</h2><h3 id="2-1-背景"><a href="#2-1-背景" class="headerlink" title="2.1 背景"></a>2.1 背景</h3><p>CPU 虚拟化范畴还挺多的，但是我们这里应该指的不包括不同 ISA 的虚拟化，而只是 CPU 的虚拟化，也就是虚拟出来的 CPU 和原本的 CPU 具有相同的 ISA 。</p><h3 id="2-2-Trap-amp-Emulate"><a href="#2-2-Trap-amp-Emulate" class="headerlink" title="2.2 Trap &amp; Emulate"></a>2.2 Trap &amp; Emulate</h3><p>为了虚拟化出多个 CPU，我们让虚拟的 OS 跑在用户态，也就是如下结构：</p><p><img src="/posts/670dae13/image-20250104222633971.png" alt="image-20250104222633971"></p><p>但是这种方式的问题在于，一旦执行到在 User mode 和 Kernel mode 行为不一致的指令的时候，那么就会导致出现 bug。</p><p>于是我们提出了 Trap&amp;Emulate 技术，它基于这样的一种观察：行为不一致的指令（被称作敏感指令）大部分都是特权指令或者访问特权寄存器，那么这种指令在 user mode 下指令，本身就会引发 trap，而 trap 到 Host OS 时，我们就用软件模拟执行的效果，也就是 Emulate。示意图如下：</p><p><img src="/posts/670dae13/image-20250104223628736.png" alt="image-20250104223628736" style="zoom:50%;"></p><p>而 Trap&amp;Emulate 技术有两点缺陷：</p><ul><li>并不是所有敏感指令都是特权指令，那么可能有些指令不会触发 trap，那么就导致这个部分 bug 了。这种行为敏感指令都是特权指令的特性被称为 strictly virtualizable，不幸的是，X86 ISA 就不是一种严格虚拟化的 ISA。</li><li>Trap 的性能开销过大。</li></ul><p>为了解决这些缺陷，我们又提出了新的技术。</p><h3 id="2-3-解决方案"><a href="#2-3-解决方案" class="headerlink" title="2.3 解决方案"></a>2.3 解决方案</h3><h4 id="2-3-1-Instruction-Interpreter"><a href="#2-3-1-Instruction-Interpreter" class="headerlink" title="2.3.1 Instruction Interpreter"></a>2.3.1 Instruction Interpreter</h4><p>也就是用软件模拟出一个 CPU 来，这样所有的指令并不是通过硬件执行的，而是通过软件模拟，这样就解决了不严格虚拟化的问题（所有的指令现在都是模拟执行了）。</p><p>Boch 就采用了这种思路。</p><h4 id="2-3-2-Binary-Translator"><a href="#2-3-2-Binary-Translator" class="headerlink" title="2.3.2 Binary Translator"></a>2.3.2 Binary Translator</h4><p>在执行代码前，需要先经过一个 translate 的过程，也就是将代码进行扫描，并将其中的敏感指令，替换成函数调用，这样就可以避免不一致问题了。</p><p>翻译的基本单位是基本块，并且翻译好的基本块会被放入 translation cache 中，下次如果还使用这个基本块的话，那么就直接使用了。至于为什么一基本块为粒度，可能是因为按指令为粒度，会频繁触发翻译拖慢速度；按可执行文件为粒度，很多执行不到的基本块其实是不需要翻译的。</p><p>Binary translation 有两个缺点：</p><ul><li>难以处理中断：在翻译后的代码中，中断只能在基本块边界处发生，而真实机器上中断可以在任何指令处发生。这可能导致精度问题，影响程序的实时性和响应能力。而且为了处理中断，需要在基本块边界保存和恢复CPU状态。这增加了上下文切换的开销和复杂性。</li><li>难以处理自修改代码（SMC）：为了检测自修改代码，必须监控对翻译后代码的写操作，这会引入额外的性能开销。</li></ul><p>如 VMware，Qemu。</p><h4 id="2-3-3-Para-virtualization"><a href="#2-3-3-Para-virtualization" class="headerlink" title="2.3.3 Para-virtualization"></a>2.3.3 Para-virtualization</h4><p>半虚拟化的设计思路是让 OS 意识到自己是一个虚拟机的 OS，对于敏感指令，就主动调一个 hypercall 自己 trap，这样就避免了被动 trap 不完全的情况。</p><h4 id="2-3-4-Hardware-Supported"><a href="#2-3-4-Hardware-Supported" class="headerlink" title="2.3.4 Hardware Supported"></a>2.3.4 Hardware Supported</h4><p>以 Intel 提供的 VT-x （x 是 eXtend 的意思）为例，它引入了 root 和 non-root 两个模式，non-root 模式下，只要遇到敏感指令，都会 trap，这样就避免了使用原本的特权机制来 trap 的缺陷。</p><p><img src="/posts/670dae13/image-20250104232230440.png" alt="image-20250104232230440" style="zoom:50%;"></p><p>此外，VTX 还提供了 VMCS ，用于保存了虚拟机的状态信息和控制信息，使 VMM 能够精确管理和恢复虚拟机的执行状态。</p><hr><h2 id="二、内存虚拟化"><a href="#二、内存虚拟化" class="headerlink" title="二、内存虚拟化"></a>二、内存虚拟化</h2><h3 id="2-1-背景-1"><a href="#2-1-背景-1" class="headerlink" title="2.1 背景"></a>2.1 背景</h3><p>首先强调，除了 <code>load</code>，<code>store</code> 指令会涉及虚拟地址的使用，<code>call</code>， <code>return</code>， <code>jump</code> 这样的指令同样会涉及虚拟地址的使用。</p><p>在虚拟化背景下，一共有 3 种地址：</p><ul><li>GVA：Guest Virtual Address</li><li>GPA：Guest Physical Address</li><li>HPA：Host Physical Address</li></ul><p>又有 3 种页表：</p><ul><li>GPT：Guest Page Table</li><li>HPT：Host Page Table</li><li>SPT：Shadow Page Table</li></ul><p>正因为有 3 种地址的存在，VM 的 GPA 并不是真实的物理地址，所以我们需要将其映射到真实的物理地址 HPA，这就是内存虚拟化要解决的问题。</p><p>下图展示了 3 种地址的关系，和两种解决办法：</p><p><img src="/posts/670dae13/image-20250107104440121.png" alt="image-20250107104440121" style="zoom:33%;"></p><h3 id="2-2-Shadow-Page-Table"><a href="#2-2-Shadow-Page-Table" class="headerlink" title="2.2 Shadow Page Table"></a>2.2 Shadow Page Table</h3><p>按理来说，因为需要完成 3 个地址之间的转换，所以需要 2 个页表，但是在早期，我们只有一个页表基地址寄存器，如 <code>CR3</code> 。所以我们如果想借助 MMU 的力量完成地址翻译，那么就需要想办法把两个页表融合成一个页表，这个页表直接完成 GVA -&gt; HPA 的地址翻译，这种页表被称作影子页表，shadow paging。</p><p>影子页表的构建就是遍历 GPT 和 HPT 的过程，其伪代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">set_cr3 <span class="token punctuation">(</span>guest_page_table<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">for</span> GVA <span class="token keyword">in</span> <span class="token number">0</span> to <span class="token number">220</span>        <span class="token keyword">if</span> guest_page_table<span class="token punctuation">[</span>GVA<span class="token punctuation">]</span> <span class="token operator">&amp;</span> PTE_P<span class="token punctuation">:</span>            GPA <span class="token operator">=</span> guest_page_table<span class="token punctuation">[</span>GVA<span class="token punctuation">]</span> <span class="token operator">&gt;&gt;</span> <span class="token number">12</span>            HPA <span class="token operator">=</span> host_page_table<span class="token punctuation">[</span>GPA<span class="token punctuation">]</span> <span class="token operator">&gt;&gt;</span> <span class="token number">12</span>            shadow_page_table<span class="token punctuation">[</span>GVA<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">(</span>HPA <span class="token operator">&lt;&lt;</span> <span class="token number">12</span><span class="token punctuation">)</span> <span class="token operator">|</span> PTE_P        <span class="token keyword">else</span>            shadow_page_table <span class="token operator">=</span> <span class="token number">0</span>     CR3 <span class="token operator">=</span> PHYSICAL_ADDR<span class="token punctuation">(</span>shadow_page_table<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>影子页表的维护也比较繁琐，主要有两个问题。一个问题是如果 VM 修改自己的 GPT 怎么办？实际上 GPT 并不存在，那么修改后不会有任何效果，所以我们需要 trap 这种修改行为（通过将 GPT 设置成只读的），并将修改同步到影子页表。</p><p>另一个问题是如果 Guest App 访问 Guest OS 的虚拟地址怎么办，因为此时 SPT 本质上是一个用户页表，所以也不存在用户地址空间和内核地址空间的隔离问题。我们可以准备两个 SPT，为 Guest App 提供的 SPT 中不包含 Guest OS 的地址映射，这样就解决了问题。</p><p>SPT 的优点在于对落后硬件的兼容性强，并且只需要一次地址翻译流程，访问时延较低。而缺点在于，对于页表的修改需要时时 trap，性能不佳。此外，SPT 的数目会非常膨胀（其实也不是太多），这是因为每个 SPT 对应一个 (GPT, HPT) 对，而每个 Guest App 都有自己的 GPT，这就导致 SPT 的数量和所有 VM 上的所有 App 一致。如果为了避免 Guest OS 地址空间被 Guest App 读写，还需要多一倍的 SPT。</p><h3 id="2-3-Direct-Paging"><a href="#2-3-Direct-Paging" class="headerlink" title="2.3 Direct Paging"></a>2.3 Direct Paging</h3><p>Direct Paging 是一种半虚拟化方法，也就是 Guest OS 使用 hypercall 来更新页表，并不能直接修改页表。页表记录的是 GVA -&gt; HPA 。</p><p>这个的好处是实现简单，而问题在于对于 VM 不透明，而且 VM 会获得更多 Host Memory 的信息，引发安全事故。</p><h3 id="2-4-Hardware-Support"><a href="#2-4-Hardware-Support" class="headerlink" title="2.4 Hardware Support"></a>2.4 Hardware Support</h3><p>硬件支持的方式就是拓展 MMU 的功能，使其可以完成二级翻译，如 Intel 的 EPT（Extended Page Table），AMD 的 NPT（Nested Page Table）。</p><p>而二级翻译最大的缺点是，二级页表的内存访问次数会过多。对于 4 级页表而言，一次地址翻译最多需要访问内存 20 次。</p><p><img src="/posts/670dae13/image-20250107123106563.png" alt="image-20250107123106563" style="zoom:50%;"></p><hr><h2 id="三、设备虚拟化"><a href="#三、设备虚拟化" class="headerlink" title="三、设备虚拟化"></a>三、设备虚拟化</h2><h3 id="3-1-背景"><a href="#3-1-背景" class="headerlink" title="3.1 背景"></a>3.1 背景</h3><p>我们希望一个真实设备可以供多个虚拟机使用，在此基础上，我们希望有如下功能：</p><ul><li>无论虚拟机想使用什么设备，我们都能模拟出来，这样便于迁移</li><li>虚拟机看到的设备应该是独占的</li></ul><p>为此我们开发了以下技术：</p><h3 id="3-2-Direct-access"><a href="#3-2-Direct-access" class="headerlink" title="3.2 Direct access"></a>3.2 Direct access</h3><p>指的是，每个 VM 都独占设备，并不存在设备同时给多个 VM 使用的情况。不过同一个设备可以在不同时间给不同的 VM 使用。</p><p>这就引出了一个问题，就是现代 DMA 设备，是可以访问物理内存的，那么一个 VM 就可以利用设备，去接触另一个 VM 的物理内存。为了解决这一点，Intel 引入了 VT-d 拓展，这个拓展提供了 IOMMU，IOMMU 里也有一套页表，用于完成 device addr 到 physical addr 的映射，切换 VM 的时候需要切换 IOMMU 中的页表，这样就限制了设备的访存能力，增强了隔离性：</p><p><img src="/posts/670dae13/image-20250104234121679.png" alt="image-20250104234121679" style="zoom:50%;"></p><p>这种 Direct Acess 的优点在于，性能非常好；而且 VMM 实现简单（基本上没有引入额外的功能）。但是缺点在于，只能提供特定的设备（host 上得有这个设备）；而且不利于拓展，如果有 100 个 VM 同时运行，难道要 100 个网卡吗？此外，也不利于 VMM 监控设备情况，因为 VM 有设备的全部控制权，VMM 不好拦截。</p><h3 id="3-3-Emulating-Devices"><a href="#3-3-Emulating-Devices" class="headerlink" title="3.3 Emulating Devices"></a>3.3 Emulating Devices</h3><p>我们也可以使用软件去模拟真实硬件（需要注意，有些模拟硬件功能的实现还是要依赖真实硬件，比如网卡收发包）。VM 使用设备时，会 trap 到 VMM，VMM 调用模拟器，如下所示：</p><p><img src="/posts/670dae13/image-20250104234817734.png" alt="image-20250104234817734" style="zoom:40%;"></p><p>这种方式的优势在于，可以模拟出多种硬件，而且允许插桩。但是缺点是性能较差。</p><h3 id="3-4-Para-Virtualized-Devices"><a href="#3-4-Para-Virtualized-Devices" class="headerlink" title="3.4 Para-Virtualized Devices"></a>3.4 Para-Virtualized Devices</h3><p>这种方式类似于 CPU 的半虚拟化，即 VM 知道自己使用的不是真实设备，而是虚拟设备。这样的好处在于，虚拟设备可以比真实设备更简单，软件栈更薄，比如说 virtio：</p><p><img src="/posts/670dae13/image-20250104235458673.png" alt="image-20250104235458673" style="zoom:50%;"></p><h3 id="3-5-Hardware-Support"><a href="#3-5-Hardware-Support" class="headerlink" title="3.5 Hardware Support"></a>3.5 Hardware Support</h3><p>我们也可以让设备自身具有虚拟化的能力（有点类似于虚拟地址空间的感觉）。这种能力被称为 SR-IOV。一个支持 SR-IOV 功能的设备，在 PCI 配置空间中呈现为“多个设备”。其物理功能部分被称为 PF，虚拟功能部分被称为 VF，如下图所示：</p><p><img src="/posts/670dae13/image-20250104235923446.png" alt="image-20250104235923446" style="zoom:30%;"></p><h3 id="3-6-总结"><a href="#3-6-总结" class="headerlink" title="3.6 总结"></a>3.6 总结</h3><p>虚拟化技术总结如下：</p><p><img src="/posts/670dae13/image-20250105000018381.png" alt="image-20250105000018381" style="zoom:40%;"></p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、CPU-虚拟化&quot;&gt;&lt;a href=&quot;#一、CPU-虚拟化&quot; class=&quot;headerlink&quot; title=&quot;一、CPU 虚拟化&quot;&gt;&lt;/a&gt;一、CPU 虚拟化&lt;/h2&gt;&lt;h3 id=&quot;2-1-背景&quot;&gt;&lt;a href=&quot;#2-1-背景&quot; class=&quot;headerlink&quot; title=&quot;2.1 背景&quot;&gt;&lt;/a&gt;2.1 背景&lt;/h3&gt;&lt;p&gt;CPU 虚拟化范畴还挺多的，但是我们这里应该指的不包括不同 ISA 的虚拟化，而只是 CPU 的虚拟化，也就是虚拟出来的 CPU 和原本的 CPU 具有相同的 ISA 。&lt;/p&gt;
&lt;h3 id=&quot;2-2-Trap-amp-Emulate&quot;&gt;&lt;a href=&quot;#2-2-Trap-amp-Emulate&quot; class=&quot;headerlink&quot; title=&quot;2.2 Trap &amp;amp; Emulate&quot;&gt;&lt;/a&gt;2.2 Trap &amp;amp; Emulate&lt;/h3&gt;&lt;p&gt;为了虚拟化出多个 CPU，我们让虚拟的 OS 跑在用户态，也就是如下结构：&lt;/p&gt;</summary>
    
    
    
    <category term="计算机系统" scheme="https://thysrael.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="计算机系统" scheme="https://thysrael.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="S9复习" scheme="https://thysrael.github.io/tags/S9%E5%A4%8D%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>计算机系统-文件系统</title>
    <link href="https://thysrael.github.io/posts/15ec58e/"/>
    <id>https://thysrael.github.io/posts/15ec58e/</id>
    <published>2025-01-02T09:33:50.000Z</published>
    <updated>2025-08-15T12:16:49.038Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、数据结构"><a href="#一、数据结构" class="headerlink" title="一、数据结构"></a>一、数据结构</h2><h3 id="1-1-Inode"><a href="#1-1-Inode" class="headerlink" title="1.1 Inode"></a>1.1 Inode</h3><h4 id="1-1-1-整体结构"><a href="#1-1-1-整体结构" class="headerlink" title="1.1.1 整体结构"></a>1.1.1 整体结构</h4><p>Inode FS 被分为了 5 个区域“</p><p><img src="/posts/15ec58e/image-20250102215509118.png" alt="image-20250102215509118" style="zoom:40%;"></p><ul><li>SuperBlock：存储着后面分区的元数据</li><li>Inode Bitmap：记录着 Inode 区的使用情况</li><li>Data Bitmap：记录着 Data 区的使用情况</li><li>Inodes：所有 Inode 的数组，使用 Inode Number 索引，每个 Inode 记录着 Data 的组织形式</li><li>Data Region：数据区</li></ul><h4 id="1-1-2-文件"><a href="#1-1-2-文件" class="headerlink" title="1.1.2 文件"></a>1.1.2 文件</h4><p>在 Inode FS 中，每个文件对应一个 Inode。</p><p>Inode 是一种与页表非常相像的数据结构，都是利用偏移量来查找对应的数据块位置，所不同的是，Inode 的翻译是不均匀的（偏移量越大，需要的翻译级数越多），而页表的翻译是均匀的（无论偏移量是多少，翻译的次数都相同）。</p><p><img src="/posts/15ec58e/image-20250102214457730.png" alt="image-20250102214457730" style="zoom: 40%;"></p><p>有一种解释是，页表翻译是硬件过程，不太灵活；而 inode 的翻译是软件过程，所以可以设计的很灵活。我是觉得之所以 inode 设计得不均匀，是因为文件的大小是不固定且偏小的，所以大部分小文件可以直接翻译或者只使用一级翻译来完成。</p><p>至于为什么页表翻译要用硬件，是因为内存访问是一个很快的事情，如果用软件实现那就太慢了；相反，因为外存的访存速度过慢，软件翻译 inode 的过程并不构成关键路径。</p><p>当外存访存速度提升时（比如说用非易失内存做外存），就会导致软件翻译的速度变慢，原本的方法就不再适用了。</p><p>除了记录拥有的数据块以外，Inode 中还记录着文件的属性。</p><h4 id="1-1-3-目录"><a href="#1-1-3-目录" class="headerlink" title="1.1.3 目录"></a>1.1.3 目录</h4><p>Inode FS 的目录项，是由文件名和文件 Inode Number 组成的。我们检索文件名，就可以得到 Inode Number，然后根据 Inode Number 去 Inode 表中检索出对应的 Inode ，就可以访问对应的数据块了：</p><p><img src="/posts/15ec58e/image-20250102214950045.png" alt="image-20250102214950045" style="zoom:40%;"></p><p><code>/</code> 的 Inode Number 为 1。</p><p>如果我们希望访问 <code>/programs/pong.c</code>，那么访问磁盘 block id 的顺序是 <code>1(/ inode) -&gt; 14 (/ data) -&gt; 7 -&gt; (program inode) -&gt; 23 (program data) -&gt; 9(pong inode) -&gt; 61 (pong data)</code>  。</p><h4 id="1-1-4-链接"><a href="#1-1-4-链接" class="headerlink" title="1.1.4 链接"></a>1.1.4 链接</h4><p>硬链接指的是两个目录项都指向同一个 Inode Number，而软链接则是创建一个文件，里面是指向文件的文件名。可以说，硬链接是文件的指针，而软链接是指针的指针。如下所示：</p><p><img src="/posts/15ec58e/image-20250102220255509.png" alt="image-20250102220255509" style="zoom:33%;"></p><p>硬链接的一个重要作用是在不拷贝文件的情况下备份文件，可以避免对于文件的误删除。软链接就不可以，因为软链接与本身的文件并不平权，原文件一旦删除，软链接也就失去作用了。</p><h3 id="1-2-FAT"><a href="#1-2-FAT" class="headerlink" title="1.2 FAT"></a>1.2 FAT</h3><h4 id="1-2-1-整体结构"><a href="#1-2-1-整体结构" class="headerlink" title="1.2.1 整体结构"></a>1.2.1 整体结构</h4><p>FAT 即 File Allocation Table。是一种 Free-List 结构。它总共有 3 个区域：</p><p><img src="/posts/15ec58e/image-20250102222430225.png" alt="image-20250102222430225"></p><ul><li>SuperBlock：对应图上的“保留区域”，也被称为 BPB（BIOS Parameter Block）。</li><li>FAT 表：记录着文件的链表元数据，本质是一个 <code>next</code> 数组。一共有 2 个相同的拷贝，互为备份。</li><li>数据区：FAT 的数据块也被叫作簇，即 Cluster 。</li></ul><h4 id="1-2-2-文件"><a href="#1-2-2-文件" class="headerlink" title="1.2.2 文件"></a>1.2.2 文件</h4><p>FAT 中每个文件的所有数据块组成一个链表，链表的 <code>next</code> 域记录在 FAT 表中。如下图所示：</p><p><img src="/posts/15ec58e/image-20250102223119987.png" alt="image-20250102223119987"></p><p>单独把 <code>next</code> 域分离出来组成 FAT 表，是因为这样可以提高访存效率。此外，空闲的簇也会在 FAT 表中组织成一个 free list 。</p><h4 id="1-2-3-目录"><a href="#1-2-3-目录" class="headerlink" title="1.2.3 目录"></a>1.2.3 目录</h4><p>FAT 目录项记录着文件的起始簇号，根据起始簇号查阅 FAT 表，就可以顺序的读出所有的数据块。</p><p>最关键的是，文件的元数据不是存在 FAT 中的，而是存在目录项中。</p><h4 id="1-2-4-链接"><a href="#1-2-4-链接" class="headerlink" title="1.2.4 链接"></a>1.2.4 链接</h4><p>FAT 系统并不支持硬链接，因为两个目录项就有两份文件的元数据，这样维护一致性就太困难了。</p><h4 id="1-2-5-与-Inode-对比"><a href="#1-2-5-与-Inode-对比" class="headerlink" title="1.2.5 与 Inode 对比"></a>1.2.5 与 Inode 对比</h4><p>Inode 的设计，采用了类似页表的方式来记录和组织磁盘块，这种方式有利于随机访问；而 FAT 用链表的方式组织磁盘块，有利于顺序访问。</p><p>我个人觉得 FAT 更容易损坏，因为链表的特性就是，一旦一个节点损坏，那么后续节点都无法访问了。这可能也是为啥 FAT 表有双备份的原因。</p><p><img src="/posts/15ec58e/image-20250102224448955.png" alt="image-20250102224448955" style="zoom:150%;"></p><hr><h2 id="二、Crash-Consistency"><a href="#二、Crash-Consistency" class="headerlink" title="二、Crash Consistency"></a>二、Crash Consistency</h2><h3 id="2-1-背景"><a href="#2-1-背景" class="headerlink" title="2.1 背景"></a>2.1 背景</h3><p>有些文件操作需要更新 data 和 metadata 两个部分，而如果在这中间发生了 Crash，可能会导致出现一致性问题，也就是 data 和 metadata 不匹配的情况。为了解决这个问题，人们开发了多种方法，下面我们会介绍一些。</p><h3 id="2-2-Journaling"><a href="#2-2-Journaling" class="headerlink" title="2.2 Journaling"></a>2.2 Journaling</h3><h4 id="2-2-1-介绍"><a href="#2-2-1-介绍" class="headerlink" title="2.2.1 介绍"></a>2.2.1 介绍</h4><p>日志（journaling）指的是先将修改写到别的地方（journal），然后将修改进行原子性的提交（commit），最后再按照日志里的内容修改文件系统。</p><p><img src="/posts/15ec58e/image-20250102225924491.png" alt="image-20250102225924491" style="zoom:40%;"></p><p>按照这种设计，如果在 journal 阶段发生 crash，那么操作只是失败了，而原本的数据依然在文件系统中完好无损。commit 阶段是原子操作，不会被 crash。而如果在 overwrite 阶段发生 crash，那么文件系统的数据会被破坏，但是我们可以根据 journal 恢复损坏的部分。</p><p>Journaling 最大的问题是所有的数据都需要写两次，这样开销是不可接受的。所以我们退而求其次，我们只对 metadata 进行 journal 。也就是先写入 Data，然后再对 MetaData 进行 journaling ，最终效果如图。</p><p><img src="/posts/15ec58e/journal2.gif" alt="journal2" style="zoom:33%;"></p><h4 id="2-2-2-Journal-Order"><a href="#2-2-2-Journal-Order" class="headerlink" title="2.2.2 Journal Order"></a>2.2.2 Journal Order</h4><p>而在实际生产中，并不是只有 disk 这一层存在的，我们会在内存中构建一个 disk cache 用于提高访问 disk 的延迟。但是这种方式会导致我们写入 disk 的时候会存在乱序现象，也就是 A 先存入 disk cache，B 后存入 disk cache，但是 B 先从 cache 中写回，而 A 后写回，则在 disk 的角度，看到的是先 B 后 A，与在应用角度看到的先 A 后 B 是矛盾的。</p><p>而 Journaling 是依靠顺序的（order），这体现在，Data 和 MetaData Journal 的写入需要在 Journal Commit 之前，而 Journal Commit 需要在 MetaData 真正写入之前。为了确保顺序，我们使用了 flush 操作，最终效果如图：</p><p><img src="/posts/15ec58e/journal3.gif" alt="journal3" style="zoom:33%;"></p><p>但是 flush 操作又是极其影响性能的一个操作，所以我们一般不 flush，任由有可能出现的不一致性发生。</p><p>OptFS 是一种学界提出的解决 flush 低效的 idea，在上文中，主要有两个地方需要维护时序。OptFS 使用 checksum 技术来保证 Commit 一定在 Data 和 MetaData Journal 的写入之后发生，使用 Delay Write（似乎是一个硬件修改），来保证 MetaData 真正写入发生在前三者之后。</p><h3 id="2-3-Shadow-Paging"><a href="#2-3-Shadow-Paging" class="headerlink" title="2.3 Shadow Paging"></a>2.3 Shadow Paging</h3><p>还有一种叫作 Shadow Paging 的方式，也叫作 Copy-on-Write，指的是当涉及到写入文件系统的时候，并不直接 in-place write（overwrite）原本的文件，而是拷贝一个新的文件并写入，写入后，让目录项从原来的文件指向这个新的文件。</p><p><img src="/posts/15ec58e/image-20250104210055159.png" alt="image-20250104210055159" style="zoom:40%;"></p><p>Journaling 是先将修改写到日志中，然后再从日志中誊抄到真正的文件系统中，而 Shadow Paging 也是不先修改原本的文件，写到另一个地方去，但是并不需要再写一遍，不过它需要复制整个文件，而日志法，可能只需要记录 diff 部分，所以性能孰优孰劣，也并不好说。</p><p>Short-Circuit Shadow Paging 是一种学界提出的优化 Shadow Paging 的方法，简单来说就是利用原子变量来进行 in-place 操作，这样就避免了对整个文件的拷贝。</p><p><img src="/posts/15ec58e/image-20250104211213687.png" alt="image-20250104211213687" style="zoom:40%;"></p><hr><h2 id="三、NVMFS"><a href="#三、NVMFS" class="headerlink" title="三、NVMFS"></a>三、NVMFS</h2><h3 id="3-1-NVM"><a href="#3-1-NVM" class="headerlink" title="3.1 NVM"></a>3.1 NVM</h3><p>NVM 即 Non Volatile Memory，非易失性存储。这个概念我觉得应该要囊括磁盘这种传统外存，和现在新型的“非易失性内存”：</p><p><img src="/posts/15ec58e/image-20250104212311015.png" alt="image-20250104212311015" style="zoom:50%;"></p><p>在授课中，我们用 NVM 表示“非易失性内存”。NVMFS 就是基于非易失性内存开发的文件系统。</p><p><img src="/posts/15ec58e/image-20250104212755464.png" alt="image-20250104212755464" style="zoom:50%;"></p><p>NVM 的优点在于不再需要复杂的软件栈，因为与传统外存相比，我们使用访存指令就可以实现数据的访问；地址索引的方式，也不需要我们设计过于复杂的数据结构。而 NVM 的缺点在于，它的价格比传统外存贵，而性能又比传统内存差，性价比不高。</p><h3 id="3-2-挑战"><a href="#3-2-挑战" class="headerlink" title="3.2 挑战"></a>3.2 挑战</h3><p>NVMFS 的有一个挑战是，现在 cache 不再是由软件负责了（disk cache），而是由硬件 cache 负责，这就导致 NVMFS 很多乱序情况都更难处理（因为不如软件好操控）。</p><hr>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、数据结构&quot;&gt;&lt;a href=&quot;#一、数据结构&quot; class=&quot;headerlink&quot; title=&quot;一、数据结构&quot;&gt;&lt;/a&gt;一、数据结构&lt;/h2&gt;&lt;h3 id=&quot;1-1-Inode&quot;&gt;&lt;a href=&quot;#1-1-Inode&quot; class=&quot;headerlink&quot; title=&quot;1.1 Inode&quot;&gt;&lt;/a&gt;1.1 Inode&lt;/h3&gt;&lt;h4 id=&quot;1-1-1-整体结构&quot;&gt;&lt;a href=&quot;#1-1-1-整体结构&quot; class=&quot;headerlink&quot; title=&quot;1.1.1 整体结构&quot;&gt;&lt;/a&gt;1.1.1 整体结构&lt;/h4&gt;&lt;p&gt;Inode FS 被分为了 5 个区域“&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/15ec58e/image-20250102215509118.png&quot; alt=&quot;image-20250102215509118&quot; style=&quot;zoom:40%;&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="计算机系统" scheme="https://thysrael.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="计算机系统" scheme="https://thysrael.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="S9复习" scheme="https://thysrael.github.io/tags/S9%E5%A4%8D%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>计算机系统-总论</title>
    <link href="https://thysrael.github.io/posts/cd771bfa/"/>
    <id>https://thysrael.github.io/posts/cd771bfa/</id>
    <published>2025-01-02T09:33:34.000Z</published>
    <updated>2025-08-15T12:16:49.037Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>那是 2023 年的夏天，楠神走出门，看向焦急等待的杰哥和我，问我们：</p><p>“你们知道“系统”和“体系结构”的区别吗？”</p></blockquote><h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>计算机系统这个系列，是我作为一名方向是 system 的研究生的，基于 IPADS 实验室开设的“计算机系统原理”等课程，整理而成的，并不保证正确性，因为 system 实在是浩如烟海，而我又太菜了。</p><p>在这篇博文里，我想谈谈我心目中的 system 是什么，这并不是 IPADS 课程的观点。</p><hr><h2 id="二、定义"><a href="#二、定义" class="headerlink" title="二、定义"></a>二、定义</h2><p>给 system 下一个定义，是我一直想干的事情。Wiki 百科和 ScienceDirect 都把 Compute System 定义为“一个由硬件、软件、数据组成的系统”。我觉得这个定义并不是很好，因为它是一句正确的废话，这个定义并不能指导科研。</p><p>还有一种方式，是将 system 定义为一套方法论和需求的集合。比如说在系统中常采用的方式是权衡、抽象、缓存、备份、并行和隔离等，而需求如下：</p><p><img src="/posts/cd771bfa/image-20250102194609112.png" alt="image-20250102194609112" style="zoom: 33%;"></p><p>这并不是全部的需求，但是好消息是 system researcher 经常在这个表中删除和增加条目。这种方式我觉得虽然和 system research 更加贴合，但是有些过于零散和变化，并没有一个核心的东西。</p><p>我想了很久，我现在觉得（也就是可能未来就改了），系统的定义是：</p><blockquote><p>在<strong>有限资源</strong>的封闭系统内，通过<strong>权衡（tradeoff）</strong>的方式，来达到我们的目的。</p></blockquote><p>对于这个定义，有如下细节：</p><ul><li>有限资源：系统并不能修改算法，因为算法可以将 $O(2^{n})$ 的方法优化成 $O(n)$ 的方法，这相当于凭空创造了资源；同样的，系统也不能修改硬件，不能优化 cache 的方式就是扩大 cache 容量，提高吞吐的方式是多买几个 GPU，这都是在系统中增加资源的方式。</li><li>权衡（tradeoff）：正因为资源是有限的，所以为了增强某项指标，就必须损害另一项指标。更进一步，为了更好的权衡，我们有诸多方法：<ul><li>牺牲一些我们不在乎的指标，增强一些我们看重的指标：其核心在于，寻找和发现那些我们并不在意的指标。</li><li>扩大系统范围：即使在当前系统中无法权衡，也可以通过将系统扩大的方式，包容进更多的资源，来进行权衡。这些资源可以是用户、硬件、算法、时间等（是的，这里违背定义的第一点了）。</li></ul></li></ul><p>在给出系统定义后，我还想在这里记录记录一下“抽象 abstract”，它不是系统的本质定义（封闭系统里的权衡），它只是多样复杂性与简单易用性的一种权衡。但是我正是出于对 abstract 和 complexity 的迷恋，而选择了 system 的方向。这种迷恋在我接触计算机之前就存在了，我小时候，是那种在熄灯后会一骨碌爬起来，恶狠狠地盯着窗帘思考这背后有什么的小屁孩。</p><p>我念研究生时，旧的时代已经过去，而新的时代还没有来临，希望它等等我。</p><p><img src="/posts/cd771bfa/image-20250102204434915.png" alt="image-20250102204434915" style="zoom:40%;"></p><hr><h2 id="三、意志"><a href="#三、意志" class="headerlink" title="三、意志"></a>三、意志</h2><p>虽然很不想承认，但是不得不承认，system 并不是自由发展的，它的发展深刻受到了社会需求和底层硬件的影响，总的来看，还是社会需求。</p><p>在最开始的时候，那时候的计算机还非常不人性化，冷冰冰的，人们研究的是操作系统内核和编程语言。</p><p>随着互联网时代的到来，人们研究的是并行、分布式、虚拟化、数据库。</p><p>而手机的普及，使得人们的研究焦点变成了安全、低功耗和异构计算。</p><p>现在 AI 时代来了，系统又会出现什么样的特征呢？这不取决于系统本身，这取决于 AI 。</p><hr>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;那是 2023 年的夏天，楠神走出门，看向焦急等待的杰哥和我，问我们：&lt;/p&gt;
&lt;p&gt;“你们知道“系统”和“体系结构”的区别吗？”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;计算机系统这个系列，是我作为一名方向是 system 的研究生的，基于 IPADS 实验室开设的“计算机系统原理”等课程，整理而成的，并不保证正确性，因为 system 实在是浩如烟海，而我又太菜了。&lt;/p&gt;
&lt;p&gt;在这篇博文里，我想谈谈我心目中的 system 是什么，这并不是 IPADS 课程的观点。&lt;/p&gt;
&lt;hr&gt;</summary>
    
    
    
    <category term="计算机系统" scheme="https://thysrael.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="知识总结" scheme="https://thysrael.github.io/tags/%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"/>
    
    <category term="计算机系统" scheme="https://thysrael.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/"/>
    
    <category term="S9复习" scheme="https://thysrael.github.io/tags/S9%E5%A4%8D%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>吃喝玩乐-三周年</title>
    <link href="https://thysrael.github.io/posts/9925eca3/"/>
    <id>https://thysrael.github.io/posts/9925eca3/</id>
    <published>2024-12-18T09:55:40.000Z</published>
    <updated>2025-09-08T08:24:44.754Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、总论"><a href="#一、总论" class="headerlink" title="一、总论"></a>一、总论</h2><p>今天是 2024 年 12 月 18 日，距离我在北航新主楼 F534 的夜晚写下这个博客的 About 板块已经过去了 3 年时间，时间过得真是太快了。</p><p>这个博客能够坚持 3 年时间，最重要的原因是有人（当然我希望小姑娘多一些）去看我的博客，博客的读者是我更新的最大动力。感谢读者们的支持和反馈，没有你们就没有“钟鼓楼”。</p><p>当时我在博客里写了“我没有报六级，也没有报冬奥会志愿者，没有小姑娘需要陪”，三年后，我居然也还是“没有报六级，也没有报冬奥会志愿者，没有小姑娘需要陪”的状态，也算是不忘初心了，乐。</p><h2 id="二、回顾"><a href="#二、回顾" class="headerlink" title="二、回顾"></a>二、回顾</h2><p>回想起来，我窝在 F534 通宵写祭祖博客的日子还历历在目。那个时候 F534 还没有被“安全检查”，是北航的一个通宵好地方，很多二系兄弟在那里做实验，打打闹闹、互相欢谑的声音是最好的解压剂；那里还有很多“住在实验室”里的人，我每次看到角落里的饭盆、折叠床和小被子，都觉得这地方比我宿舍还有生活气息，我觉得就算丧尸爆发，F534 也可以靠这些物资成为末世之光，不过这个幻想在安全检查后破灭了；当然也有不是那么开心的事情，那里有可能会刷新出可恶的情侣，他们不但互相啃来啃去的，四条腿混在一起我都分不清谁是谁的。他们甚至还会抽空泡个面条，大冬天的腾起一个宏大的雾柱，这对于当时形单影只的我是一个巨大的刺激。</p><p>在我写完祭祖博客以后，我认识了很多很好的朋友，也当上了祭祖助教，可以说是春风得意了。如果你那时候就认识我，你或许可以看到我在北航的校园里，骑着那辆特别漂亮的邮差二八大杠，摇头晃脑地去图书馆写博客。我这段时间写了基本上将所有我想写的东西都写了个遍，我那段时间简直入魔了。现在我想起来我和我 OO 助教拍胸脯保证的要写的那些博客，都感觉非常羞耻，我那个时候想过写“IDEA 的快捷键”，但是直到现在，我其实都没有搞懂过 Java 项目的管理工具，我当时是怎么有自信纠结这种无聊的细节的？奥，我想起来了六系 21 级水群刚成立的时候，我在群里半推半让、暗戳戳地安利我自己的博客的时候的搞笑嘴脸，从外人角度看上去，应该还挺猖狂的吧。</p><p>再后来就到了大三下学期，我接了三个活儿：罗杰软工、操作系统比赛、保研。那段时间真的是啊，罗杰软工榨干了所有我的写博客的动力，当然其他事情其实也扎破了我为我自己吹起的幻象泡泡。那个学期我几乎没有更新我的博客，后面也没有补上。我对于大三下学期，我觉得是我做错了很多事情，我不过就是一个幼稚的孩子，喜欢提着别人的领子去问“我对没有对？你错没有错？”。再深入的思考我也没有勇气再进行了，我不知道最后的那个结果我是不是可以接受（这么看我比大一结束的时候还怂）。</p><p>后来我进了新的实验室，到现在也是这样，新实验室的有很多有趣的知识，任务也很紧（主要原因是我太菜了，本科四年光玩了），很少有时间能坐下来写博客了。而且新的知识相比于授课知识，也变得更加零碎无体系化，我将这些知识记录到 <a href="https://thysrael.github.io/obsidian-quartz/">roam</a> 里面，我也不确定这是一种形式上的进化，还是一种心灰意懒的妥协。</p><p>奥对了，后来那辆二八大杠我再也不骑了，那辆车的链条很久没有膏过油了，我再也骑不动了。我之前还喜欢在匿名提问箱里写东西，后来我收到了一个提问，他说他看完了我的所有回答，说我是一个虚伪的人。我不知道怎么回答这个提问，所以也不再用提问箱了。其他的事情，我也没有思考出什么确定的结论来，瑷。</p><h2 id="三、未来"><a href="#三、未来" class="headerlink" title="三、未来"></a>三、未来</h2><p>上交这边的课真的很有意思，无论是分布式和并行计算相关的课程，还是 Firmware 的课程，甚至是自然辩证法，经常是上完课后恨不得一拍大腿，说“原来是这样子的，我之前真是井底之蛙呀！”。我记了很多笔记，希望有时间能够整理好发到博客上，真的超级有趣！自然辩证法相关的东西已经被写到 roam 上了，但是其他的，我更希望是以正式博文的形式表达出来，但是这样难度很高。</p><p>另外这个学期我补充了很多西方的历史，把很多之前在脑子中零散的概念都梳理出来了，感觉以后吹逼小故事可以更加真实了，要是也有时间总结出来就好了。</p><p>另外我还希望写一些关于《金瓶梅》的东西，但是它真的好难读啊，每天下班后脑子晕晕的，根本读不进去呀！！！</p><h2 id="四、其他"><a href="#四、其他" class="headerlink" title="四、其他"></a>四、其他</h2><p>我有想过停更的，一直以来支持我写博客的动力都是有人看我的博客，但是，似乎今后再也没有人会看我写的东西了，我现在都不知道我在做什么，怎么能指望读者会知道呢？</p><p>但是我看到最刚毅的人使用了迂回的方式，最聪明的人选择了无聊，最真诚的人让人生厌。我觉得事情不应该是这样的，这个世界上，一定会有人，也应当有人，选择一条路走到黑，选择痛苦而不是无聊，而真诚，也应当有好的回报。所以我还是希望写下去，写到这样的人看到我的博客。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一、总论&quot;&gt;&lt;a href=&quot;#一、总论&quot; class=&quot;headerlink&quot; title=&quot;一、总论&quot;&gt;&lt;/a&gt;一、总论&lt;/h2&gt;&lt;p&gt;今天是 2024 年 12 月 18 日，距离我在北航新主楼 F534 的夜晚写下这个博客的 About 板块已经过去了 3 年时间，时间过得真是太快了。&lt;/p&gt;
&lt;p&gt;这个博客能够坚持 3 年时间，最重要的原因是有人（当然我希望小姑娘多一些）去看我的博客，博客的读者是我更新的最大动力。感谢读者们的支持和反馈，没有你们就没有“钟鼓楼”。&lt;/p&gt;
&lt;p&gt;当时我在博客里写了“我没有报六级，也没有报冬奥会志愿者，没有小姑娘需要陪”，三年后，我居然也还是“没有报六级，也没有报冬奥会志愿者，没有小姑娘需要陪”的状态，也算是不忘初心了，乐。&lt;/p&gt;
&lt;h2 id=&quot;二、回顾&quot;&gt;&lt;a href=&quot;#二、回顾&quot; class=&quot;headerlink&quot; title=&quot;二、回顾&quot;&gt;&lt;/a&gt;二、回顾&lt;/h2&gt;</summary>
    
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    
    <category term="吃喝玩乐" scheme="https://thysrael.github.io/tags/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"/>
    
    <category term="S9课上" scheme="https://thysrael.github.io/tags/S9%E8%AF%BE%E4%B8%8A/"/>
    
  </entry>
  
</feed>
