<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://enigmahuang.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://enigmahuang.me/" rel="alternate" type="text/html" /><updated>2026-02-24T07:05:47-08:00</updated><id>https://enigmahuang.me/feed.xml</id><title type="html">Rainmaker’s Notebook</title><subtitle>求雨巫师的笔记本</subtitle><author><name>Rainmaker&apos;s Notebook</name></author><entry><title type="html">CUTLASS：我们走了一些弯路</title><link href="https://enigmahuang.me/posts/2026/02/CuTeDSL-new-feat/" rel="alternate" type="text/html" title="CUTLASS：我们走了一些弯路" /><published>2026-02-26T00:00:00-08:00</published><updated>2026-02-26T00:00:00-08:00</updated><id>https://enigmahuang.me/posts/2026/02/CuTeDSL-new-feat</id><content type="html" xml:base="https://enigmahuang.me/posts/2026/02/CuTeDSL-new-feat/"><![CDATA[<p>去年我在 <a href="https://enigmahuang.me/posts/2025/05/NV-against-NV/">英伟达反对英伟达</a> 一文里面吐槽过 CUTLASS 的易用性。大半年后的今天，我们看到了一些有意思的变化。</p>

<h2 id="不易用性">（不）易用性</h2>

<p>去年下半年 CuTeDSL 上线后立即收到了大量的关注，连 Flash Attention 4 都是用 CuTeDSL 实现的。和 CUTLASS C++ 相比，CuTeDSL 终于摆脱了 C++ 模板地狱，导致各位搓 kernel 的兄弟再也没有『等编译』这个合理摸鱼的理由了。这解决了 CUTLASS C++ 的一大痛点，但是另一大痛点，也是 CUTLASS 3.0 设计最突出的特点和优点，CuTe layout, 其易用性问题依然没有解决。以 v4.4 的 Blackwell FP16 GEMM <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/examples/python/CuTeDSL/blackwell/tutorial_gemm/fp16_gemm_0.py">教程代码</a> 为例。</p>

<p>大部分 CUDA kernel 或者说几乎所有需要用 Tensor Core 的 kernel, 主要做三件事：1. 划分全局数据，把自己需要使用的数据从 gmem 读取到 smem 或者 reg 里 (prologue)；2. 用 smem / reg 里面的数据进行计算 (compute/mainloop)；3. 把计算得到的结果写回全局 gmem (epilogue). 选用 Blackwell 代码而非 Hopper 代码是因为我觉得 Blackwell 拆分 mainloop 和 epilogue 比 Hopper 不拆分的更合理和更好理解。大部分情况下，一个线程组 (CTA) 从 gmem 读写的数据都可以视作一个矩阵的子块。 Triton, cuTile 等 tile-based / array-based 编程语言基本上也是按这样来划分函数功能组别的。那么在 CuTeDSL 里面呢？以上面的代码为例，和输入矩阵 $A$ 直接相关的局部变量，分别有：<code class="language-plaintext highlighter-rouge">mA_mkl</code>, <code class="language-plaintext highlighter-rouge">gA</code>, <code class="language-plaintext highlighter-rouge">sA</code>, <code class="language-plaintext highlighter-rouge">tCgA</code>, <code class="language-plaintext highlighter-rouge">tCrA</code>, <code class="language-plaintext highlighter-rouge">tAsA</code>. 而和输出矩阵 $C$ 直接相关的局部变量，分别有：<code class="language-plaintext highlighter-rouge">mC_mnl</code>, <code class="language-plaintext highlighter-rouge">gC</code>, <code class="language-plaintext highlighter-rouge">tCtAcc</code>, <code class="language-plaintext highlighter-rouge">tCtAcc_epi</code>, <code class="language-plaintext highlighter-rouge">gC_epi</code>, <code class="language-plaintext highlighter-rouge">tDtC</code>, <code class="language-plaintext highlighter-rouge">tDgC</code>, <code class="language-plaintext highlighter-rouge">tCrAcc</code>, <code class="language-plaintext highlighter-rouge">tCrC</code>. 啊，为什么 <code class="language-plaintext highlighter-rouge">tCgA</code> 和 $A$ 相关但是不是和 $C$ 相关？<code class="language-plaintext highlighter-rouge">tCgA</code> 和 <code class="language-plaintext highlighter-rouge">tCrA</code> 的区别是什么？为了方便理解，我画了一张变量依赖关系图：</p>

<p><img src="http://enigmahuang.github.io/files/CuTeDSL-new-feat/SM100-GEMM-dep-1.png" alt="SM100-GEMM-dep-1" /></p>

<p>实际上，CUTLASS 里面有大量形如的 <code class="language-plaintext highlighter-rouge">tXlY</code> 的变量命名，其中 <code class="language-plaintext highlighter-rouge">X</code> 和 <code class="language-plaintext highlighter-rouge">Y</code> 和划分对象有关，<code class="language-plaintext highlighter-rouge">l</code> 和存储位置有关，但是 CUTLASS 的文档里从来没有解释过应该如何解读这样的变量名。让我们暂时忍一下种类繁多的中间变量，进一步了解一下一些核心函数。比如最核心的变量之一，<code class="language-plaintext highlighter-rouge">tiled_mma = cute.make_tiled_mma(op)</code>. 查看 <a href="https://docs.nvidia.com/cutlass/latest/media/docs/pythonDSL/cute_dsl_api/cute.html">CUTLASS 文档</a> 可知这个函数返回了一个 <code class="language-plaintext highlighter-rouge">TiledMma</code> 类，这个类派生自 <code class="language-plaintext highlighter-rouge">MmaAtom</code> 类, 包含 <code class="language-plaintext highlighter-rouge">get_slice</code>, <code class="language-plaintext highlighter-rouge">make_fragment_{A,B,C}</code> 等常见且核心的操作。然而官方文档里，这两个类的成员和方法函数没有任何说明：没有说明输入参数有什么要求，没有说明成员的功能和期待的输出是什么、满足什么条件。</p>

<p><img src="http://enigmahuang.github.io/files/CuTeDSL-new-feat/CUTLASS-doc-1.png" alt="CUTLASS-doc-1" /></p>

<p><img src="http://enigmahuang.github.io/files/CuTeDSL-new-feat/CUTLASS-doc-2.png" alt="CUTLASS-doc-2" /></p>

<p>且慢， CuTeDSL 提供了一些调试用的打印功能，可以打印出一些变量信息帮助理解。比如下面这一段代码（148-155行）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># (MMA, MMA_M, MMA_K)
</span><span class="n">tCrA</span> <span class="o">=</span> <span class="n">tiled_mma</span><span class="p">.</span><span class="n">make_fragment_A</span><span class="p">(</span><span class="n">sA</span><span class="p">)</span>
<span class="c1"># (MMA, MMA_N, MMA_K)
</span><span class="n">tCrB</span> <span class="o">=</span> <span class="n">tiled_mma</span><span class="p">.</span><span class="n">make_fragment_B</span><span class="p">(</span><span class="n">sB</span><span class="p">)</span>
<span class="c1"># (MMA, MMA_M, MMA_N)
</span><span class="n">acc_shape</span> <span class="o">=</span> <span class="n">tiled_mma</span><span class="p">.</span><span class="n">partition_shape_C</span><span class="p">(</span><span class="n">mma_tiler_mnk</span><span class="p">[:</span><span class="mi">2</span><span class="p">])</span>
<span class="c1"># (MMA, MMA_M, MMA_N)
</span><span class="n">tCtAcc</span> <span class="o">=</span> <span class="n">tiled_mma</span><span class="p">.</span><span class="n">make_fragment_C</span><span class="p">(</span><span class="n">acc_shape</span><span class="p">)</span>
</code></pre></div></div>

<p>如果我们打印出 <code class="language-plaintext highlighter-rouge">tCrA</code> 和 <code class="language-plaintext highlighter-rouge">tCrB</code> 的信息，可以看到它们的形状是 <code class="language-plaintext highlighter-rouge">(1,1,4,4)</code>. 其中最后一个 4 是 <code class="language-plaintext highlighter-rouge">MMA_K = mma_tiler_mnk[2] / mma_inst_shape_mnk[2]</code>, 即在一个分块里面 K 维度方向需要使用多少次 MMA atom; 倒数第二个 4 其实是代码里设定的 <code class="language-plaintext highlighter-rouge">ab_stage = 4</code>, 那么前面的两个 1 呢？注释里标注的可是一个三维元组，不是一个四维元组。把 <code class="language-plaintext highlighter-rouge">tCtAcc</code> 也打印出来，可以看到它的形状为 <code class="language-plaintext highlighter-rouge">((128,256),1,1)</code>. 这倒是好理解一点：<code class="language-plaintext highlighter-rouge">(128,256)</code> 就是 $C$ 矩阵的分块大小，对应注释里的 <code class="language-plaintext highlighter-rouge">MMA</code>. 但为什么和上面的 <code class="language-plaintext highlighter-rouge">MMA</code> 不同？</p>

<p>写到这里我不禁要再次感叹：世界上到底有多少聪明的大脑能完全搞懂 CuTe 背后到底在干什么？</p>

<h2 id="我们在探索中走了一些弯路">『我们在探索中走了一些弯路』</h2>

<p>CUTLASS v4.4 带来了一些 CuTeDSL 的实验特性：数据划分逻辑简化、自动生成 TMA 描述符、无 fragment 编程模式、pipeline 接口简化等。这个 <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/examples/python/CuTeDSL/experimental/blackwell/dense_gemm.py">新样例代码</a> 展现了大部分的新特性。这个新的样例代码写了大量的注释来解释这些操作，很值得一读，个人觉得比此前所有的样例代码都更有价值。在仔细阅读这个样例代码之前，不妨先看看下面这张变量关系依赖图：</p>

<p><img src="http://enigmahuang.github.io/files/CuTeDSL-new-feat/SM100-GEMM-dep-2.png" alt="SM100-GEMM-dep-2" /></p>

<p>和上一张变量关系依赖图相比，可以看到新代码大幅减少了涉及的变量，而且 prologue 和 mainloop 部分的逻辑也大幅度简化了，以及数据流动的方向也用带箭头的虚线标记了出来。</p>

<p>对于 TMA 复制，新代码提供了一些非常方便的新接口：<code class="language-plaintext highlighter-rouge">make_smem_layout_{a,b}</code> 和 <code class="language-plaintext highlighter-rouge">make_tmem_layout_acc</code>. 和自动生成 TMA 描述符搭配，在 prologue 数据复制和 mainloop 计算这两个部分，代码写起来已经比较接近 Triton/cuTile 这些语言了，对降低程序员心智负担有极大的帮助。诚然，这些辅助函数背后还是有一些更复杂的操作，但是大部分时候程序员并不真的需要知道或者更改实际上的数据排布，编程框架就应该提供一个选项把这些复杂度隐藏起来。 Epilogue 部分仍然相对复杂，这主要是由于：1. 数据流动的阶段相对较多：要先从 tmem 复制到 reg, 在 reg 里面完成后处理，再复制到 smem, 最后用 TMA 写回 gmem; 2. 涉及到多次数据排布格式 (CuTe layout) 的更改。这个 epilogue 比教程代码里的实现更复杂，因为不是直接从 reg -&gt; gmem, 而是经 smem 用 TMA 进行写入。但是如果和 <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/examples/python/CuTeDSL/blackwell/dense_gemm.py">性能更好的样例代码</a> 里的 <code class="language-plaintext highlighter-rouge">epilogue_tma_store()</code> 实现对比，那也是大幅度简化了。考虑到 epilogue 的模式都比较固定，如果以后能有进一步的辅助函数，类似现有的用于 <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/examples/python/CuTeDSL/blackwell/dense_gemm_persistent.py#L1006">dense_gemm_persistent.py</a> 里的 <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/python/CuTeDSL/cutlass/utils/gemm/sm100.py#L157">epilogue_tma_store</a> 一样的辅助函数，那将更进一步降低复杂度。</p>

<p>一个顺理成章的问题是，如果 CuTeDSL 在未来提供更多的辅助函数和简化的接口，那么它和 Triton/cuTile 这些框架的区别是什么？目前看来，CuTeDSL 还可以手动控制 pipeline 深度，以及做一些更细颗粒度的调度和操作（比如 <a href="https://github.com/NVIDIA/cutlass/blob/3476ddb7bd6ca4161a0169103ceaa20ce0eb891f/examples/python/CuTeDSL/blackwell/dense_blockscaled_gemm_persistent.py#L368">overlap and double buffer accumulator</a>）。Triton/cuTile 目前依赖编译器的 auto wrap specialization, 会在编译和运行过程中试图对操作自动进行切片和将不同操作的切片进行流水线拼接。这样的灵活性和最后的性能，在短期内可能还是会比手动控制的要稍微差一点。</p>

<p>总而言之，随着新语言特性和接口的引入，CuTeDSL 在解决易用性方面，又迈出了重要的一步。或许再过几年，随着编译器的发展，tile-based DSL 就能挑起大梁了呢？那就真的说不好是『好时代，来临了』还是坏时代来临了。</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="GPU" /><category term="AI" /><summary type="html"><![CDATA[去年我在 英伟达反对英伟达 一文里面吐槽过 CUTLASS 的易用性。大半年后的今天，我们看到了一些有意思的变化。]]></summary></entry><entry><title type="html">DeepSeek Multi-head Latent Attention 计算流程图</title><link href="https://enigmahuang.me/posts/2025/12/DeepSeek-MLA/" rel="alternate" type="text/html" title="DeepSeek Multi-head Latent Attention 计算流程图" /><published>2025-12-14T00:00:00-08:00</published><updated>2025-12-14T00:00:00-08:00</updated><id>https://enigmahuang.me/posts/2025/12/DeepSeek-MLA</id><content type="html" xml:base="https://enigmahuang.me/posts/2025/12/DeepSeek-MLA/"><![CDATA[<p>近日因工作需要去学习了一下大名鼎鼎的 DeepSeek Multi-head Latent Attention. MLA 的计算流程比标准的 GQA 要复杂不少，主要是处理低秩压缩和 RoPE. 搜了一些网上的资料，没有看到非常满意的计算流程图，因此自己动手画了两个。下面两张图都是针对推理的，不考虑反向传播计算梯度。第一张图是不使用矩阵吸收时的计算流程（常用于 prefill），第二张图是使用矩阵吸收时的计算流程（常用于 decode）。图中各个张量的大小都做了标注，维度名字大部分遵循 <a href="https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/config.json">DeepSeek V3 官方参数名</a>. 部分张量的 <code class="language-plaintext highlighter-rouge">head_dim</code> 和 <code class="language-plaintext highlighter-rouge">seq_len</code> 的位置可能会交换，不影响理解；矩阵乘法的内积维度未显式标出。</p>

<p><img src="http://enigmahuang.github.io/files/MLA/MLA-no-absorb.png" alt="MLA without matrix absorb" /></p>

<p><img src="http://enigmahuang.github.io/files/MLA/MLA-with-absorb.png" alt="MLA with matrix absorb" /></p>

<p>DSv3 参数：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">hidden_size</code>: 7168</li>
  <li><code class="language-plaintext highlighter-rouge">kv_lora_rank</code>: 512</li>
  <li><code class="language-plaintext highlighter-rouge">q_lora_rank</code>: 1536</li>
  <li><code class="language-plaintext highlighter-rouge">num_attention_heads</code>: 128 （图中写作 <code class="language-plaintext highlighter-rouge">num_heads</code>）</li>
  <li><code class="language-plaintext highlighter-rouge">qk_nope_head_dim</code>: 128</li>
  <li><code class="language-plaintext highlighter-rouge">qk_rope_head_dim</code>: 64</li>
  <li><code class="language-plaintext highlighter-rouge">v_head_dim</code>: 128</li>
</ul>

<p>我对照了 vLLM 里的代码执行路径，发现和我的流程图稍有不同，主要是对矩阵吸收的应用。其中，吸收/不吸收的共通路径在
<a href="https://github.com/vllm-project/vllm/blob/33278073d68940dcaff579ab2dc316700e1db87a/vllm/model_executor/layers/mla.py#L110-L173">vllm/model_executor/layers/mla.py, L110-L173</a>
内部分支实现在
<a href="https://github.com/vllm-project/vllm/blob/33278073d68940dcaff579ab2dc316700e1db87a/vllm/v1/attention/backends/mla/common.py#L1927-L2114">vllm/v1/attention/backends/mla/common.py, L1927-L2114</a>
共通路径部分大体和我流程图里的 Common part 相同，但是总是对 $c^Q_t$ 做 up projection ($q^C_t = c^Q_t \times W^{UQ}$). 实际上，vLLM 的矩阵吸收方式不是 $(c^Q_t \times W^{UQUK}) \times (c^K_t)^T$, 而是 $((c^Q_t \times W^{UQ}) \times W^{UK}) \times (c^K_t)^T$, 这样依然避免了显式计算出 up projection 以后的 $k^C_t = W^{UQ} \times (c^K_t)^T$. 带入 DSv3 的具体参数，可以计算得到 vLLM 这种方式所需的计算量更小。同样，在使用矩阵吸收的时候，会给 MQA 的输出乘以 $W^{UV}$, 最后回到共通部分再乘以 $W^{O}$ 得到最终输出。</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="AI" /><summary type="html"><![CDATA[近日因工作需要去学习了一下大名鼎鼎的 DeepSeek Multi-head Latent Attention. MLA 的计算流程比标准的 GQA 要复杂不少，主要是处理低秩压缩和 RoPE. 搜了一些网上的资料，没有看到非常满意的计算流程图，因此自己动手画了两个。下面两张图都是针对推理的，不考虑反向传播计算梯度。第一张图是不使用矩阵吸收时的计算流程（常用于 prefill），第二张图是使用矩阵吸收时的计算流程（常用于 decode）。图中各个张量的大小都做了标注，维度名字大部分遵循 DeepSeek V3 官方参数名. 部分张量的 head_dim 和 seq_len 的位置可能会交换，不影响理解；矩阵乘法的内积维度未显式标出。]]></summary></entry><entry><title type="html">东北往事（美国）组图</title><link href="https://enigmahuang.me/posts/2025/06/US-NE-Trip/" rel="alternate" type="text/html" title="东北往事（美国）组图" /><published>2025-06-25T00:00:00-07:00</published><updated>2025-06-25T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2025/06/US-NE-Trip</id><content type="html" xml:base="https://enigmahuang.me/posts/2025/06/US-NE-Trip/"><![CDATA[<p>去美国东北部的老革命根据地进行了一次深度学习，大有收获。下面是一些印象深刻的时刻，与诸君共享。</p>

<h3 id="界限分明-波士顿-central-station">《界限分明》, 波士顿 Central station</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/1.jpg" alt="" /></p>

<h3 id="走投无路-波士顿-central-station">《走投无路》, 波士顿 Central station</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/2.jpg" alt="" /></p>

<h3 id="三世三统-波士顿美术馆">《三世三统》, 波士顿美术馆</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/3.jpg" alt="" /></p>

<h3 id="原神冲击-波士顿某个-7-11-便利店">《原神冲击》, 波士顿某个 7-11 便利店</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/4.jpg" alt="" /></p>

<h3 id="师祖遗训-麻省理工学院">《师祖遗训》, 麻省理工学院</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/5.jpg" alt="" /></p>

<h3 id="梦中所思-纽约-33th-st-path-station">《梦中所思》, 纽约 33th St PATH station</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/6.jpg" alt="" /></p>

<h3 id="美国经济-纽约证券交易所侧门">《美国经济》, 纽约证券交易所侧门</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/7.jpg" alt="" /></p>

<h3 id="当代艺术-费城艺术博物馆">《当代艺术》, 费城艺术博物馆</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/8.jpg" alt="" /></p>

<h3 id="引颈受戮-美国国家历史博物馆">《引颈受戮》, 美国国家历史博物馆</h3>

<p><img src="http://enigmahuang.github.io/files/US-NE-Trip/9.jpg" alt="" /></p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="Travel" /><summary type="html"><![CDATA[去美国东北部的老革命根据地进行了一次深度学习，大有收获。下面是一些印象深刻的时刻，与诸君共享。 《界限分明》, 波士顿 Central station 《走投无路》, 波士顿 Central station 《三世三统》, 波士顿美术馆 《原神冲击》, 波士顿某个 7-11 便利店 《师祖遗训》, 麻省理工学院 《梦中所思》, 纽约 33th St PATH station 《美国经济》, 纽约证券交易所侧门 《当代艺术》, 费城艺术博物馆 《引颈受戮》, 美国国家历史博物馆]]></summary></entry><entry><title type="html">英伟达反对英伟达</title><link href="https://enigmahuang.me/posts/2025/05/NV-against-NV/" rel="alternate" type="text/html" title="英伟达反对英伟达" /><published>2025-05-17T00:00:00-07:00</published><updated>2025-05-17T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2025/05/NV-against-NV</id><content type="html" xml:base="https://enigmahuang.me/posts/2025/05/NV-against-NV/"><![CDATA[<p>本文仅代表作者本人观点，与作者所属的任何团体皆无关系。本文内容不构成任何明示或默示之建议。</p>

<h2 id="deepseek-一声炮响">DeepSeek 一声炮响</h2>

<p>2025 年 2 月 26 日，DeepSeek 开源周第三天，DeepGEMM 代码开源发布。围绕 DeepSeek 开源代码的讨论有很多，最抓人眼球的是『DeepSeek 打破 CUDA 壁垒』、『DeepSeek 撕开英伟达垄断圈』一类的说法。稍微懂行的人都知道此类说法基本都是新闻学魅力时刻，严肃正经的反驳和讨论也已有很多。泥沙混流之中，还有一些有趣的东西值得捞起来看一看。</p>

<p>打开 <a href="https://github.com/deepseek-ai/DeepGEMM">DeepGEMM 代码仓库</a>，README 文件第二段有这么一句话：</p>

<blockquote>
  <p>While it leverages some concepts from CUTLASS and CuTe, it avoids heavy reliance on their templates or algebras.</p>
</blockquote>

<p>这里面出现了两个名字：CUTLASS 和 CuTe. CUTLASS 的全称是 CUDA Templates for Linear Algebra Subroutines, 看起来和 Basic Linear Algebra Subroutine (BLAS) 颇为相似。实际上 CUTLASS 并不这么 Linear Algebra, 它的目的只有一个：搓出性能最好的 AI kernel, 主要是 GEMM. 传统的 cuBLAS 库也在与时俱进提供各种新的 GEMM 实现，然而受制于接口和发布的形式，cuBLAS 能支持的应用场景还是不如 CUTLASS. 如果需要定制开发 kernel, 比如 DeepGEMM 所需要的 FP8 fine-grained scaling, CUTLASS 理应是最好的选项。当然，Triton 也能定制开发 kernel, 甚至作为一门 DSL, Triton 用起来还更简单一点。但是除了 OpenAI 自己可以关起门来优化，有多少人能把 Triton kernel 写得和 CUTLASS kernel 一样快？至于 CuTe, 我们稍后再展开，只要知道它自 CUTLASS 3.0 开始是 CUTLASS 的一个核心组件。</p>

<p>那么问题来了：DeepGEMM 只是借用了一些 CUTLASS 和 CuTe 的组件，而没有用 CUTLASS 来实现，代价是什么？</p>

<h2 id="走进新时代">走进新时代</h2>

<p>2022 年，NVIDIA 发布了新一代 Hopper 架构的旗舰计算卡 H100. 比起前一代 A100, H100 的理论峰值性能，特别是 Tensor Core (TC) 性能，有了巨大的跃进：以 SXM 版本为对比，FP16/BF16/INT8 翻了三倍，从 312/312/624 T 变成了 939/939/1959 T, 同时引入了了高达 1959T 的 FP8 支持。硬件性能大跃进，软件自然也要跟上：H100 上引入了新的 WGMMA 指令集。对应地，CUTLASS 3.0 系列发布，TC 编程走进了新时代。</p>

<p>讨论何为『新时代』之前，有必要先回顾一下 good old times. 在 V100 和 A100 时期，TC 编程使用的指令是 WMMA/MMA. 这些指令的执行粒度都是单个 wrap, 需要 wrap 里面所有线程一起执行同一条指令。这种方式在别的 CUDA 库里面也有，比如 <a href="https://nvidia.github.io/cccl/cub/api/classcub_1_1WarpReduce.html">CUB 的 WrapReduce</a>. 使用 WMMA/MMA 实现的 GEMM kernel, 比如<a href="https://github.com/wzsh/wmma_tensorcore_sample/blob/master/matrix_wmma/matrix_wmma/main.cu">这个样例代码</a>，看着和传统的 GEMM kernel 还有几分相似，只是颗粒度变大了一点。一个熟练的 CUDA 码农，对着文档和样例学个一两天也就能上手了。</p>

<p>到了 Hopper 时代，好消息是传统的 MMA/WMMA 还可以继续用，坏消息是如果只用这些传统指令，代码最多最多只能跑到<a href="https://hazyresearch.stanford.edu/blog/2024-05-12-tk">大概 63% 的峰值性能</a>。而 H100 上面，每四个 wrap 组成了一个 quadrant, 里面有一个 wrap scheduler, 一个 TC, 512 个向量寄存器，和一些其他部件。WGMMA 对应的 wrap group 就是四个 wrap. 此外，还有全新的 tensor memory accelerator (TMA), 以及 TMA 支持的 memory swizzling. 更让人头痛的是，WGMMA 的 PTX 文档不仅难读懂，甚至还有错（见前一个链接）。往好了说，写文档的人自己都神情恍惚搞错了。更更吓人的是，为了让 TC 保持全速计算，WGMMA 出现了 wrap specialization, 即一个 wrap 专门负责搬运数据，其他 wrap 负责控制 TC 做计算。对应地，GPU 上的软件流水线出现了。</p>

<p>能够直接硬啃 PTX 然后手搓 WGMMA 的人实在是凤毛麟角。于是 CUTLASS 3.0 包装好了所有底层操作，变成了事实上的 H100 TC 编程文档和基本接口。CUTLASS 3.0 还带来了一个新的核心部件 CuTe, 用以 “describe and manipulate tensors of threads and data”. 利用 CuTe 和 CUTLASS 官方实现和包装好的各种功能，开发者应该可以像搭积木一样组合和实现出各种功能的 kernel, 并且方便地调试各种模板参数来把性能推向极致。如此一来，可谓是以改兼振两难自解，我大明天下无敌啊！</p>

<p>平心而论，面对日益复杂的硬件架构和日益增加的功能需求，CUTLASS 3.0 所做的只是计算机科学两大方法论里的第一条：增加一层抽象以实现更多功能。但是同样平心而论，CUTLASS 3.0 着实有些抽象了。CuTe 的抽象可以很简洁地描述某些复杂的数据排布，但是 CuTe 这一套复杂的 layout algebra, 除了十多份官方文档，还有一篇英伟达员工在此之上另外写的一万七千字的<a href="https://leimao.github.io/article/CuTe-Layout-Algebra/">讲解文章</a>, 来帮助其他开发者学习 CuTe. 而 CUTLASS 本身，则是一个将 C++ 模板编程用到极致的典型。一个 kernel 有十几个 class 作为模板参数，VSCode language server 看了都崩溃，更别说人了。而这些大大小小的 class 和模板，也没有多少有文档，基本上只能靠看代码和看样例来推测其用法。哎，没事，写算子的朋友们只要苦一苦就好了，TC 要做的事情可就多了。</p>

<h2 id="革命与自我革命">革命与自我革命</h2>

<p>总体而言，H100 + CUTLASS 3.0 时代，硬件和软件的复杂性问题都开始浮出水面。传统的 (GP)GPU 的硬件设计，对应的并行编程模式是 SIMT, 依靠在大量轻量级的线程中进行切换和轮流执行以掩盖显存访问延迟和保持算术单元一直进行计算。这一范式在 H100 TC 上面已经被打破了，wrap specialization 和对应的 CUTLASS 里的多级软件流水线，和传统的 GPU 编程已经是完全不同的范式。始作俑者其无后乎，H100 打破了 V100 A100 的计算模式以获得性能上的大跃进，一堆 kernel 搓出来还没用多久，Blackwell 的 TC 架构和对应的编程模式又改了。虽然 Hopper 到 Blackwell 的变动远没有 Ampere 到 Hopper 的变动那么大，但是各种 kernel 还是得重新搓一次。那么 Blackwell 之后的 Rubin 呢？要做出多少牺牲、妥协、变化？</p>

<p>在软件层面，CUTLASS 团队显然也意识到了现在 CUTLASS 有多难用, DeepGEMM 没有用 CUTLASS 就是最好的例证。于是，CUTLASS 4.0 开始引入 Python DSL 的支持。<a href="https://docs.nvidia.com/cutlass/media/docs/pythonDSL/overview.html">官方原话</a>如下：</p>

<blockquote>
  <p>CUTLASS 4.x bridges the gap between productivity and performance for CUDA kernel development. By providing Python-based DSLs to the powerful CUTLASS C++ template library, it enables faster iteration, easier prototyping, and a gentler learning curve for high-performance linear algebra on NVIDIA GPUs.</p>
</blockquote>

<p>CUTLASS 有 Python DSL 是好事。毕竟如果延续现在这个学习难度和开发复杂度，加上 Blackwell 开始支持的 MX 数据类型和各种 sub-type 数据类型所需的操作，本就紧张的 CUTLASS 产能很可能会开始拖后腿。但是 CUTLASS Python DSL 似乎有些迟了，因为更激进的技术路线已经出现了：外有 <a href="https://github.com/tile-ai/tilelang">tile-lang</a>, 内有 GTC 25 上面公布的 <a href="https://www.linkedin.com/posts/brycelelbach_we-just-announced-cutile-a-tile-programming-activity-7308545706242306048-Lp_j/">cuTile</a>. 随着 cuTile 一起到来的还有 Tile IR, 这个东西看着可就有意思了：</p>

<p><img src="http://enigmahuang.github.io/files/NV-against-NV/TileIR.jpg" alt="Tile IR" /></p>

<p>在这张示意图里面，cuTile -&gt; Tile IR 被称为 Tile path, 完全和 SIMT path 不同，也不走 PTX. cuTile 这种 “array-based paradigm” 里面为传统 CUDA 编程范式留下了多少容身之地，我们可以暂且打个问号。如果以后 cuTile 发展顺利，能在性能上达到一流水平，那么绝大部分需要给 NVIDIA GPU 编程的程序员就只需要学习 cuTile, 不需要学习传统的 CUDA 编程就可以满足工作需求了。换言之，旧时代的 CUDA 生态护城河，可能就会变成一道马奇诺防线了。</p>

<p>脑洞不妨再开得大一些。如果软件上的 DSL 大获成功，那么硬件设计上面会不会也日益 DSA 化？毕竟 NV 面临着每一代旗舰卡理论峰值性能起码翻一倍的压力，否则资本市场不会买账。半导体制程提升的红利越来越小，单个芯片的面积越来越大，砍掉和矩阵计算无关的单元的潜在收益会越来越高。比如，旗舰卡上的 ROP 和 TMU 单元明显是不需要的; 需要用 CUDA core 执行的计算也没有那么多，只要保留一部分来执行少量非矩阵运算就够了。如果以后真的变成了 DSA + DSL, 那么事情就变得有趣起来了。</p>

<p>最后是一个值得思考的问题：AMD 的 HIP 也好，没有后续的 zluda 项目也罢，还有各种国产显卡上号称『兼容 CUDA』的编程框架，面对 SIMT/WMMA/WGMMA/TCGEN5, 兼容哪些才算『兼容 CUDA』? 如果英伟达自己都不打算在最重要的产品上面兼容自己以前的编程模式，你们『兼容 CUDA』的意义又在哪里？</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="GPU" /><category term="Hardware" /><category term="AI" /><summary type="html"><![CDATA[本文仅代表作者本人观点，与作者所属的任何团体皆无关系。本文内容不构成任何明示或默示之建议。]]></summary></entry><entry><title type="html">湾区爬山小结</title><link href="https://enigmahuang.me/posts/2025/01/bay-area-hiking/" rel="alternate" type="text/html" title="湾区爬山小结" /><published>2025-01-25T00:00:00-08:00</published><updated>2025-01-25T00:00:00-08:00</updated><id>https://enigmahuang.me/posts/2025/01/bay-area-hiking</id><content type="html" xml:base="https://enigmahuang.me/posts/2025/01/bay-area-hiking/"><![CDATA[<p>来湾区小半年，附近的山头爬得七七八八了。做个小盘点，供诸君参考。</p>

<h1 id="1-大宝荐">1. 大宝荐</h1>

<h2 id="rancho-san-antonio-county-park--open-space-preserve">Rancho San Antonio County Park &amp; Open Space Preserve</h2>

<p>停车和步行起始点：S Meadow Trail, Cupertino, CA 95014 (PG&amp;E Trailhead)</p>

<p>路径高点/折返点：High Meadow Vista Point</p>

<p>到湾区以后爬的第一个小山头，学长带我去的。这座山的步行道估计是南湾周末最多人来的几个步行道之一，就像白云山之于广州人一样。这条步行道路好走，距离可长可短，有好几段路视野宽阔。夏天可能有几段路有点晒，不过湾区嘛晒一下问题不大。最大的问题是周末早上九点半以后可能不好找停车位。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanAntonio1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanAntonio2.jpg" alt="" /></p>

<p>山下还有一个 Deer Hollow Farm, 是一个示范性质的小农庄，由政府资助的志愿者运营。哎，老美就是不会做生意啊，这要是在国内早就搞个农家乐了。也可能是来这里的人太多了，搞农家乐接待不过来。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanAntonio3.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanAntonio4.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanAntonio5.jpg" alt="" /></p>

<h2 id="mission-peak">Mission Peak</h2>

<p>停车和步行起始点：680 Stanford Ave, Fremont, CA 94539 (Mission Peak Trail Head)</p>

<p>路径高点/折返点：Mission Peak Pole</p>

<p>『湾区三大俗』有好几个版本，无论哪个版本都少不了 hiking, 而 hiking 的典型代表就是 Mission Peak. 俗不俗啊，太俗了。景色好不好啊，确实好。几乎整条山路都有极其良好的视野，随时停下都能看景。接近山顶那一段比较陡峭，走起来要小心一点。Mission Peak 是我来湾区爬的第二座山，一个人爬山走得比较快，七十分钟登顶，六十分钟下来。后来又和其他朋友一起爬了一次，接近两个小时才走上去，一个半小时下来。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak2.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak3.jpg" alt="" /></p>

<p>山顶可以清晰看到南湾。</p>

<p>圣诞节前还有人搬了一颗小圣诞树上来山顶。冬天了，山也开始变绿了，终于不是屎黄色了。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak4.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak5.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/MissionPeak6.jpg" alt="" /></p>

<h2 id="sierra-vista-open-space-preserve">Sierra Vista Open Space Preserve</h2>

<p>停车和步行起始点：Rustic Lands Parking / Alum Rock Park Parking</p>

<p>路径高点/折返点：Boccardo Loop Trail</p>

<p>这座山不算很高，和 Mission Peak 相近。个人觉得爬起来比 Mission Peak 容易一点，然而这座山的视野比 Mission Peak 差一些，多少让我觉得有点一时瑜亮。但是我去的那一次着实被一场可遇不可求的景观惊艳到了。这条步行道在山谷中，容易起云雾。那天开始爬山的时候是阴天，在山脚下看山顶还有云雾（划重点，这个这个伏笔待会要回收）。爬到三分之一看到山顶亮了，云雾开始消散。爬到山顶，看到了半山腰高度一片绵延过去覆盖南湾的云海！我感觉这是非常可遇不可求的景色。这一次的观景体验可以位列我在湾区爬山观景体验的第一名。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SierraVista1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SierraVista2.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SierraVista3.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SierraVista4.jpg" alt="" /></p>

<p>更好的消息是，爬上去的山头对面不远处还有一处观景平台，可以开车沿 Sierra Rd 上去到 Boccardo Loop Trail. 有个朋友得知以后开车上去看了一次落日，回来大加称赞。</p>

<h2 id="san-bruno-mountain-state--county-park">San Bruno Mountain State &amp; County Park</h2>

<p>停车和步行起始点：谷歌地图搜公园同名地点</p>

<p>路径高点/折返点：Kits-FM San Francisco</p>

<p>SFO 机场北面的小山。不算高，有步行道和汽车道可以上到山顶。山顶视野极佳，有超过 270 度的视野。西面是 Daly City 和浩瀚无边的太平洋。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanBruno1.jpg" alt="" /></p>

<p>北可看到旧金山和金银岛，美中不足的是金门大桥被旧金山西南的 Twin Peaks 挡住了看不见。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanBruno2.jpg" alt="" /></p>

<p>东可眺望 Mt Diablo 俯瞰奥克兰，下图右侧黑色的山顶就是 Mt Diablo.</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/SanBruno3.jpg" alt="" /></p>

<p>东南方向被山体挡住，看不见 SFO 机场和南湾。</p>

<h2 id="uc-berkeley-后山">UC Berkeley 后山</h2>

<p>呃，我也不知道应该如何称呼这座山。当时是去伯克利找朋友玩，然而伯克利校园并没有多少好逛的，我们就顺便爬了一下后山。从伯克利学校出发，一直走到 The Lawrence Hall of Science 然后折返。不高，有公路可以开车上去。近处有著名的劳伦斯伯克利国家实验室不过没什么好看的，远处是旧金山、金银岛、天使岛和金门大桥。不过能不能看到金门大桥取决于天气。总体而言，能够比较清楚地看到湾区北边的景色。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/Berkeley1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/Berkeley2.jpg" alt="" /></p>

<h2 id="lands-end---golden-gate-national-recreation-area">Lands End - Golden Gate National Recreation Area</h2>

<p>停车和步行起始点：靠近 Seal Rocks 的停车场</p>

<p>折返点：China Beach</p>

<p>这个严格意义上不算山，不过考虑到是在海边而且高度落差达到了五十米，勉强将其划入爬山的行列。虽然名字是天涯海角，然而我觉得一点都不恰当，无论从哪个方向上都算不上 lands end. 这地方往南走一点是 Ocean Beach, 可能夏天适合玩水。走步行道则主要是看看金门海峡入口和金门大桥。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/LandsEnd1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/LandsEnd2.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/LandsEnd3.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/LandsEnd4.jpg" alt="" /></p>

<p>不得不吐槽一下几个沙滩，都有一些充满涂鸦的废弃建筑，看着就很吓人。沙滩和海水也不怎么干净。还有人在沙滩上钓鱼，我很怀疑这个地方的浪和水深到底有没有鱼。</p>

<h2 id="fremont-older-open-space-preserve">Fremont Older Open Space Preserve</h2>

<p>停车和步行起始点：Stevens Creek Chestnut Parking Lot</p>

<p>折返点：Maisie’s Peak</p>

<p>就在 Rancho San Antonio County Park 东南边不远处。如果 PG&amp;E Trailhead 人太多了没有停车位可以来爬这座山。山下有个水库，还挺好看的。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/FremontOlder1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/FremontOlder2.jpg" alt="" /></p>

<h1 id="2-就当是体育锻炼吧">2. 就当是体育锻炼吧</h1>

<h2 id="black-mountain">Black Mountain</h2>

<p>湾区周围有好几个叫 Black Mountain 的山。我当时脑子进水，开车绕了一圈山路到 Skyline Ridge Parking Lot 停车，然后走到 Black Mountain Top Radio Tower 以后折返。全程基本上只能看到西南面得山，只有到山顶的一小段能看到北面的湾区。实际上在这个 Radio Tower 北边还有一个 Black Mountain Trail, 可以从 Rhus Ridge Trailhead 走上来，这样视野上会好一些。不过再好也和东边紧挨着的 Rancho San Antonio County Park 的路线相差无几。所以这山爬得确实只能是当体育锻炼了。</p>

<h2 id="mt-el-sombroso">Mt El Sombroso</h2>

<p>停车和步行起始点：Alma Bridge Rd &amp; Limekiln Trail</p>

<p>路径高点/折返点：Mt El Sombroso</p>

<p>这座山是湾区附近最高的几座山之一。这条路线也很长，来回十八公里。我一个人去，走了四个小时。此路线视线极差，几乎全程都被南北两面的山遮蔽，只有靠近山顶的一小段可以眺望南湾。部分路段比较陡，也比较晒。纯纯的体育锻炼路线。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/ElSombroso1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/ElSombroso2.jpg" alt="" /></p>

<h1 id="3-爬了吗如爬">3. 爬了吗？如爬</h1>

<h2 id="mt-hamilton">Mt Hamilton</h2>

<p>来到湾区最高峰——哈密尔顿！太美丽了家人们。哎呀这不是天文台吗，还是看看远处的海湾吧家人们。</p>

<p>海拔 1283 米的湾区最高峰！还有世界上首个建于山顶的永久性天文台利克天文台。本来我在爬了 Mt El Sombroso 以后想继续挑战一下自己，从山脚的 Twin Gates Trailhead 开始爬上来。后来看了一下卫星图，没有步行道，只有弯弯绕绕的公路，走得太危险，只好作罢。这公路开上去也不好开。山顶有不止一个天文台，实际上有一群。有志愿者讲解最早的那台利克望远镜，这台望远镜经过维护现在还能使用。白天天文学家都在睡觉，他们晚上才起来干活。值得表扬的还有山顶的免费望远镜，国内景区望远镜从来都要收钱。这望远镜可以看到南边 Mt Umunhum 山上的雷达站基座遗址，往北隐隐约约可以看到金门大桥。然而开下山回到湾区平原还得再开起码四十分钟，要是在山上看落日，回程就难免晚上开山路了。</p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/Lick1.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/Lick2.jpg" alt="" /></p>

<p><img src="http://enigmahuang.github.io/files/bay-area-hiking/Lick3.jpg" alt="" /></p>

<h2 id="mt-diablo">Mt Diablo</h2>

<p>大名鼎鼎的大菠萝山，应该是湾区附近唯一有步行道能爬的海拔超过一千米的山。圣诞节后的那个周末我和基友去爬此山。前一天看天气预报说阴天，我心想这估计和我爬 Sierra Vista Open Space Preserve 一样，山顶能看到云海。再退一步，阴天就阴天嘛，不下雨就行，上去应该还是能远眺一下周围风景。结果那天去到以后发现半山腰以上全部笼罩在云雾里，停车所在的 Rock City Park 已经颇有些海拔高度，往上走没多久就开始进到云雾里。更糟糕的是，有些步行道非常泥泞，完全无法正常通行。如果走公路上去，距离要增加很多，当天的时间不允许我们走那么长距离。最后无奈只好开车到山顶。山顶就全是过山风了，云雾呼啸而过，头顶能看到蓝天，但是周围就是能见度不足二十米。最后参观了一下山顶的袖珍游客中心，就灰溜溜地开车打道回府了。</p>

<p>由于天气实在不适合拍照，因此没！有！照！片！</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="Outdoor" /><summary type="html"><![CDATA[来湾区小半年，附近的山头爬得七七八八了。做个小盘点，供诸君参考。]]></summary></entry><entry><title type="html">这个学就上到这里了</title><link href="https://enigmahuang.me/posts/2024/08/goodbye-school/" rel="alternate" type="text/html" title="这个学就上到这里了" /><published>2024-08-16T00:00:00-07:00</published><updated>2024-08-16T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2024/08/goodbye-school</id><content type="html" xml:base="https://enigmahuang.me/posts/2024/08/goodbye-school/"><![CDATA[<p>近日收到了博士学位证书，我的学生生涯如无意外的话就此结束了。借此机会，谈一点近来的思考。</p>

<p>我大抵算不得一个成功的博士生。一方面，我在学校里呆的时间太长，整整花了七年才拿到博士学位。另一方面，我读博期间的研究工作，颇算得上形散神不聚——虽然我设法在毕业论文里把大部分工作在一个主题下面串到了一起，但这些工作并没有合起来解决某个大的问题。在我看来，在学术研究上，一个成功的博士生应该将一个较大的问题分解为若干可以解决的子问题，在数年的研究中逐步解决这些问题，最终将子问题上的进展合并到一起，解决一个大的问题。这很难，更多的人并不能这样自顶向下解决问题，而是自底向上：选择一个感兴趣的方向，做出了一些关联或大或小的研究，最后放到一起加以连接变成一篇博士论文。我也是是自底向上完成博士论文的芸芸众生之一。不论是自顶向下还是自底向上，都绕不开很重要的一步：提出『合适』的问题。</p>

<p>正如爱因斯坦所说：『提出一个问题往往比解决一个问题更重要』。只要给问题加上足够的限定，就很容易合理化自己的工作和突出自己的成果。论文前面介绍研究背景和相关工作的部分，对于一些热门问题或者老问题可以说是写烂了。那为什么还要写呢？笔则笔削则削。对相关工作不足之处的评价是定义问题的一部分，那对论文而言自然是极为重要的。问题定得『好』，论文灌水灌到饱。即使计算机科学的绝大部分方向都是实践性的，我们依然能见到每年产生的大量没有实用意义的研究。比如某些研究为每一组给定的输入数据花费大量时间生成特定优化代码（如果你知道 SpMV, 那么没错我说的就是这个问题）。这些研究往往号称其代码生成时间可以被多次运行的收益所平均和抵消，比如花几个小时的时间搜索所有可行方案组合生成代码把某个操作的单次运行时间从 0.1 秒降低到 0.05 秒。你赢赢赢，最后就是被阿姆达尔定律这颗寒天之钉直接砸下来插个透心凉。每次看着某些经典老问题又被人百尺竿头更进一步，再回头看看那些广为使用的软件包里所用的甚至连做对比基线都没资格的方法，真是不得不感叹战报会说谎但战线不会。</p>

<p>让我们从科研本身往后退一步。科研中出现的靠定义问题来躺赢的途径，在近年某些社会思潮中有很明显的体现，那就是通过争夺对问题的定义权来『解决』问题。日光之下并无新事，《论语》的『必也正名』，《韩非子》的『循名责实』，《庄子》的『名止于实』，都在强调『名』的重要性。对问题的定义极大限制了解答问题的途径和答案。比如说，『P 社玩家是不是都是战犯？』是一个基于刻板印象的谬误，其答案应该是否定的。但是如果我从贝叶斯概率的角度来提问：『已知某人是 P 社玩家的前提下，此人是战犯的概率是否高于某一阈值？』，那么答案就很可能是肯定的了。很多问题就像这样，虽然相似，但是并不等价。非学术的交流里存在大量基于对话者所处环境和背景的上下文和内容省略，然而这些省略的部分包含了对真正问题的定义和限制。对问题定义和评价标准的思考和讨论，很多时候是比选择问题和解决问题更重要的元问题。在我看来，现实之中，大部分人在面临大部分问题的时候，既没有意识也没有能力讨论这个元问题。</p>

<p>这又让我想到了前几年流行起来，如今依然被广泛使用的一个词：『（小镇）做题家』。狭义上来说，这个词指的是擅长做题和应试但不擅长解决实际问题的人，且这些人大多缺乏视野或者资源。他们大多认为通过『做题』可以解决很多问题，因此遭到批判。这是一种典型的『挑战-回应范式』，以前西方学者研究中国近代史常用这个范式，也是常用的一类网络安全认证方法。挑战未必都是题目，但题目和考试都是挑战。做题家习惯了单一的挑战来源和评价体系。久而久之，很多人便习惯认为挑战的种类是单一的，价值评判体系也是单一的。如果稍微推广一点，应试并不是『做题家』唯一做的事情，他们还习惯、擅长乃至乐于在单一评价体系解决评价体系提出的问题并获得好的评价。</p>

<p>要做一个做题家，一则需要熟练掌握解决某一种类问题的技巧，二则需要对评价体系有充足的理解。在技术和实操层面，解决很多实际问题离不开大量的做题家。如果只考虑工具属性，做题家并不坏，甚至很好。美国学生里的做题家要比国内的少，但很多人连成为做题家都做不到。以我读研期间做助教的经历来看，很多美国学生的知识基础堪忧，符合网上比比皆是的相关讨论。我无意在此探讨教育系统和教育体制的问题，这些问题太大了。从个体的角度来看，做题家最大的问题是他们只体现出了自己的工具属性。对做题家来说，别人出什么题他们就做什么题，那他们就只有做不完的题了。对工具属性的批判也早已存在：『君子不器』。</p>

<p>应该指出，我并不认为做题家就不懂独立思考。解决问题和独立思考并不矛盾，重点在于思考的内容。做题家往往只思考如何解决问题，而问题并不只存在于题面，还存在于题外。两个重要的题外题是：这是必须要解的一道题吗？这道题的『评分标准』是怎么定的？对绝大多数中国学生来说，确实只有升学这一道题可以解。升读国外的大学基本上也只是换了一套题目的考试。到了大学后，可以解的题就多了：除了升学，还可以不同的就业方向和创业。选择的权利自然伴随着为选择承担责任的义务，这两点在中小学教育里长期缺位，造就了大量不想自己做出选择并承担责任的人。在选择问题之上，『评分标准』则是一个更难企及的元问题。为什么评分标准是这样？谁有权决定这样的评分标准？这就回到了前文讨论的问题的定义权。这些问题是做题家极少会去思考的问题，也是『出题人』不愿意做题家思考的问题。</p>

<p>做题家的命运我们已经见得很多，也反思得很多了。另一方面，虽然说『新周故宋，以《春秋》当新王』，然而素王终究不是王，只会念经并不能真正解决问题。变换定义虽然能解决『问题』，但并不能真正解决问题。人类要发展和进步，有些问题终归在那里，总是需要合适的方法和技能去解决。在提升『做题技巧』之余，我们也应该常常想想题面之外的问题。解正确的题永远比正确地解题更重要。走出做题的舒适区固然充满困难和挑战；人类的赞歌正是勇气的赞歌。</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="Parallel Computing" /><summary type="html"><![CDATA[近日收到了博士学位证书，我的学生生涯如无意外的话就此结束了。借此机会，谈一点近来的思考。]]></summary></entry><entry><title type="html">某显卡公司的面试题目</title><link href="https://enigmahuang.me/posts/2024/08/GPU-company-interview-questions/" rel="alternate" type="text/html" title="某显卡公司的面试题目" /><published>2024-08-15T00:00:00-07:00</published><updated>2024-08-15T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2024/08/GPU-company-interview-questions</id><content type="html" xml:base="https://enigmahuang.me/posts/2024/08/GPU-company-interview-questions/"><![CDATA[<h2 id="前言">前言</h2>

<p>卖艺难，卖身更难，今年求职情况可谓惨不忍睹。去年年底我申了 24 所北美大学的 CS 教职，喜提全拒德（严格意义上来说，还有 20 所至今没有给我发拒信）。去年和今年我都去系里的 job talk 蹭了不少饭，眼看着应聘者的引用数从 $[200, 2000]$ 的区间暴涨到 $[500, 5000]$, 而且所有来做 job talk 的人做的东西都和 AI 沾边。二月份赶紧跑路找工业界打螺丝的岗位，先后投了如下公司：</p>

<ul>
  <li>硬件公司：AMD, Intel, NVIDIA, HPE</li>
  <li>互联网公司：Amazon, Google, Microsoft, Apple, ByteDance</li>
  <li>工业公司：ANSYS, DE Shaw Research</li>
</ul>

<p>只有三个公司给了我面试机会，都是因为有强力内推才拿到的面试。最后我面完了某显卡公司的两个职位，一个面了九场，一个面了八场外加两个线下四十八小时代码优化测试。下面是这两个职位的面试中的一些题目，仅供后来诸君参考。由于所有题目均为面试以后回忆记录，可能部分题目描述不够充分。另外，这两个职位的面试都很有针对性，没有任何 LeetCode 或其他很常见的算法题出现在任何一场面试中。</p>

<h2 id="数据结构">数据结构</h2>

<ol>
  <li>
    <p>写一个数据结构支持 <code class="language-plaintext highlighter-rouge">void enqueue(一个函数指针，void *arg, size_t arg_size)</code> 和 <code class="language-plaintext highlighter-rouge">void drain(调用所有 enqueue 的函数和 arg)</code>, 每次 enqueue 的时候只能发生一次 malloc. 更进一步，实现的代码能否支持在传进 enqueue 的函数里调用 enqueue, 如果要支持需要怎么实现。</p>
  </li>
  <li>
    <p>有一个发送器 @ 1GHz, 有一个接收器 @ 0.8 GHz. 假设发送器每 100 cycles 里面有 80 个发包 20 个闲置，如果采用一个 FIFO buffer, 这个 buffer 最小要有多大？</p>
  </li>
  <li>
    <p>一个浮点数数组从前往后加和从后往前加得到的结果是否一样？</p>
  </li>
  <li>
    <p>给一千万个 FP16 数值进行排序，最快的串行算法是什么，其时间和空间复杂度是什么？</p>
  </li>
</ol>

<h2 id="系统">系统</h2>

<ol>
  <li>
    <p>Pinned/unpinned host memory access 的区别是什么？</p>
  </li>
  <li>
    <p>用 compare-and-swap 原子指令实现一个 atomic add.</p>
  </li>
  <li>
    <p>用原子操作指令实现一个简单的 mutex.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">sched_yield()</code> 和 <code class="language-plaintext highlighter-rouge">sched_resume()</code> 这两个函数的用法。</p>
  </li>
  <li>
    <p>实现一个无锁队列支持多生产者多消费者模型。进一步，如果每次入队的数据量超过一个 cache line 的大小，应该如何正确实现？</p>
  </li>
  <li>
    <p>如何设计 GPU-aware 的 <code class="language-plaintext highlighter-rouge">MPI_Send()/MPI_Recv()</code> （即这可以直接将 GPU 内存指针传入这两个函数）? 进一步，如果我们需要多次发送，每次都初始化 IPC handle 会很浪费时间，如何解决？ 如果还是多次发送，但是 buffer 会变，如何处理？</p>
  </li>
  <li>
    <p>给一个异构系统写一个 <code class="language-plaintext highlighter-rouge">malloc()/free()</code> 函数，可以在 CPU 或者 GPU 上面分配和释放内存。但是硬件架构可能是 CPU 直连 GPU，也可能是通过 Root Complex 和 Switch 连接。仅可以调用系统的 <code class="language-plaintext highlighter-rouge">mmap()/munmap()</code> 和 GPU 上类似的函数。</p>
  </li>
</ol>

<h2 id="并行计算">并行计算</h2>

<ol>
  <li>
    <p>什么是 Amdahl’s law? 如果一个串行程序有 95% 运行时间对应的部分可以并行化，此程序最大的并行加速比是多少？</p>
  </li>
  <li>
    <p>有 $N$ 个不同的不可拆分的计算任务，第 $i$ 个任务耗时 $i$ 单位，如果有足够多核心，最小运行时间多少？如果可以选核心数，最快但是最不空置核心的并行方法是什么？</p>
  </li>
  <li>
    <p>CPU 和 GPU 硬件架构的主要区别是什么？</p>
  </li>
  <li>
    <p>GPU 编程有什么要注意的影响性能的地方？</p>
  </li>
  <li>
    <p>设计一个算法在 GPU 上并行解压 (n_repeat, char) 这样的 token 对。每个 stream 有许多这些 token 对，可能有一个或者多个 stream. 例： (3, ab), (5, c) 解压后的输出为 abababccccc. 这个算法有什么性能问题？</p>
  </li>
  <li>
    <p>写一个 GPU 上的并行矩阵转置函数，给出优化版本和说明优化原因。</p>
  </li>
  <li>
    <p>写一个 GPU 上的并行排序函数。</p>
  </li>
</ol>

<h2 id="硬件">硬件</h2>

<ol>
  <li>
    <p>一个 CPU 连着 PCIE switch 1 和 PCIE switch 2. 每个 PCIE switch 分别连着一个 NIC 和一个 GPU. 两个 NIC 之间有网线，可以 RDMA. 若 PCIE switch 1 上的 NIC 向 CPU 报告其完成了一个将 GPU 1 显存中的数据进行 RDMA put 到 GPU 2 显存上的操作，CPU 是否立即可以告知 GPU 2 有一些新的可用数据？</p>
  </li>
  <li>
    <p>同上一问的硬件拓扑结构。若 NIC 1 向 GPU 2 的显存进行 RMDA put, 有无数据需要经过 CPU? （注：考点疑似是 IOMMU）</p>
  </li>
  <li>
    <p>什么是 Base Address Register (BAR), 其作用是什么？</p>
  </li>
  <li>
    <p>单级 cache CPU, cache size 4MB, 初始化一个 2MB 数组和一个 8MB 数组，哪一个的运行时间更短？</p>
  </li>
  <li>
    <p>8-way associated 和全相连 cache 的区别是什么，它们各自有什么优缺点？</p>
  </li>
  <li>
    <p>假设在一个 FP32 峰值性能为 10 TFlops 的 GPU 上做单精度矩阵乘法，并且已知输入输出矩阵所需内存远大于 GPU 显存。GPU 通过 16GB/s 的 PCIE 连接到 CPU, 理论上最少需要多少显存才足够让 GPU 能够跑满浮点数峰值计算性能？</p>
  </li>
  <li>
    <p>写一个 CPU 上的单线程矩阵转置函数。这个实现会产生多少对主内存的访问？</p>
  </li>
  <li>
    <p>为什么 L1 缓存即使不考虑价格也不能无限大？</p>
  </li>
</ol>

<h2 id="网络通信">网络通信</h2>

<ol>
  <li>
    <p>MPI 中单边 (get, put) 和双边 (send, recv) 操作的主要区别是什么？什么时候选择单边/双边操作？</p>
  </li>
  <li>
    <p>MPI 中的小鹰模式和约会模式的区别是什么？</p>
  </li>
  <li>
    <p>MPI one-sided 和 SHMEM 的主要区别是什么？</p>
  </li>
  <li>
    <p>用 MPI one-sided 操作和 <code class="language-plaintext highlighter-rouge">MPI_Barrier()</code> 实现一个 <code class="language-plaintext highlighter-rouge">MPI_Allreduce()</code>, 可以假设进程数是二的幂。分析其时间复杂度。如果不允许使用 <code class="language-plaintext highlighter-rouge">MPI_Barrier()</code>, 如何实现？</p>
  </li>
</ol>

<h2 id="一般性的编程问题">一般性的编程问题</h2>

<ol>
  <li>
    <p>C++ 虚函数是什么，编译器如何处理虚函数的跳转？ （注：考点应该是vtable）</p>
  </li>
  <li>
    <p>交换 a b 两个整数，不用第三个寄存器，最少要多少次什么操作？进一步，将 a b c 变为 c a b, 不用第四个寄存器，要多少次什么操作？</p>
  </li>
  <li>
    <p>$N$ 个数里找第 $k$ 小的数（$k$ 是一个预先知道的固定的很小的正整数），应该用什么算法，时间复杂度是什么？</p>
  </li>
  <li>
    <p>排序 $N$ 个取值范围是 $[0, 1000]$ 的整数，时间复杂度是什么？假设有 $P$ 个核心来并行化这一操作，其时间复杂度是什么？</p>
  </li>
  <li>
    <p>堆栈模拟递归实现和真递归实现哪个快？堆栈模拟的入堆元素比递归的少，少多少？哪些寄存器不需要入堆？</p>
  </li>
</ol>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="Parallel Computing" /><category term="GPU" /><category term="Hardware" /><category term="Job" /><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">SC23：公款旅游 once more</title><link href="https://enigmahuang.me/posts/2023/11/SC23/" rel="alternate" type="text/html" title="SC23：公款旅游 once more" /><published>2023-11-19T00:00:00-08:00</published><updated>2023-11-19T00:00:00-08:00</updated><id>https://enigmahuang.me/posts/2023/11/SC23</id><content type="html" xml:base="https://enigmahuang.me/posts/2023/11/SC23/"><![CDATA[<p>虽然今年没有中论文，但是机缘巧合，又可以去 SC23 公款旅游了。此时此刻，恰如<a href="https://enigmahuang.me/files/old-blog-archive/2019-SC19%EF%BC%9A%E5%B9%B4%E8%BD%BB%E4%BA%BA%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%85%AC%E8%B4%B9%E6%97%85%E6%B8%B8.pdf">四年前的彼时彼刻</a>，同样是没有论文，同样是去丹佛。好在今年丹佛天气很好，白天大都是晴天，有十多度接近二十度，也没有下雨下雪什么的。坏在今年丹佛天气真的像天气预报那么好，而我对天气预报的心存疑虑导致我多带了不少衣服。四年过去以后，我感觉丹佛最大的变化就是流浪汉变多了。无论是从机场进市区的铁道途径的地方，还是市区街道转角的小花园，如今都多了不少流浪汉。考虑到我在贝洛伯格也翻了不少垃圾桶，推己及人或许我应该称他们『开拓者』。</p>

<p>第四次去 SC, 已经没什么兴趣去逛展了。老板不来，本想跟着老板去找大牛们刷刷脸，如今只好自己顶硬上。事实证明，找大牛蹭脸，哪怕大牛以前不认识我，也比去 Job Fair 要有用得多。跟大牛听同一个 paper session 并且多问一些有质量的问题，还有一点短期 buff 的作用。Job Fair 展台坐着的人基本只能回答一些宽泛和简单的问题，派些宣传册之类的，最后基本都会告诉学生去网上看他们的职位列表和投简历。我打印了八份简历去丹佛，最后只投出了四份，其中还有两份不是在 Job Fair 上投的。RIKEN 在 Job Fair 的展台还找了个欧洲人来接待，日语听力是练不成了。那欧洲人倒也很懂，我一问他 RIKEN 的工作环境感觉如何，他马上就说 RIKEN 的工作环境不像那些典型的日企，外国人也能适应。然而 RIKEN 还是摆脱不了日本人年功序列那一套，霍金来了也得先做五年博士后，然后再升研究员。至于工资水平，啊哈哈，咱还是别谈这些伤感情的话题了。</p>

<p>今年从哈利橙那里听到了一些 SCC 的鬼故事。不知道是哪个大聪明提议的，SCC 今年开始对安全有所强调。今年组委会会有人尝试日各个队伍的机器，日进去了要扣分。今年比赛的神秘应用甚至直接就是 capture the flag, 提供了一百多 G 的 wireshark 数据要各个队伍分析。哈利还提到说他们的牙膏厂 8 系 SPR CPU 随着散热器的不断压紧先后出现了没插内存条的内存插槽报内存训练错误和 PCIE 设备消失的问题，好在最后都能用。</p>

<p>今年听了+看了不少论文。中国石油大学刘伟峰教授的 SSSLab 的稀疏 LU 分解器 PanguLU 拿到了<a href="https://dl.acm.org/doi/10.1145/3581784.3607050">最佳论文</a>，这是国内团队首次在 SC 拿到最佳论文，可喜可贺。后来看到国内的文章，原来 PanguLU 已经开发三年了，在 Solver 21 的时候就面世了。国内这个 Solver 会议很有意思，有点像欧美的 preconditioning conference 和 fast direct solver conference，我记得以前看过他们提出的求解器问题集，里面有些问题确实有意义。Solver 会议还带了一个 solver challenge 比赛，也很有助于结合理论和实践。另外刘老师这个组的论文里的示意图都很好看，喜欢在第一页放一张概括性的图，感觉跟 Hoefler 老爷的风格有点像。这个组甚至人力充足到给 PanguLU 设计了一个图标。</p>

<p>水的论文也有一些，可能是 experiment track 的，但实在是有点水。有一篇测稀疏矩阵重排序对 SpMV 性能影响的<a href="https://dl.acm.org/doi/10.1145/3581784.3607046">论文</a>，居然只用了一个教科书级别 naive 的 1D CSR SpMV 和一个 2D （甚至都没有动态负载均衡）, 甚至不愿意测一下一些新的广为人知的算法如 CSR5. 这论文倒也有个有趣的地方，他们不知道从哪里搞来了华为 CPU 加入了测试；华为 CPU 的绝对性能赶不上 Intel 和 AMD, 但是用图划分或者超图划分重排以后往往能获得很好的提升。另一个被测试的 ARM 处理器也有类似的情况。还有一篇<a href="https://dl.acm.org/doi/10.1145/3581784.3607096">论文</a>我真不知道怎么过审的，一个用代码生成器生成代码算分布式内存并行张量积的工作，绝口不提前一年 SC22 的 Deinsum 这个相关工作，在问答环节被人问起来直接回答不知道这个工作。AMD 讲他们如何给自己的机器优化 HPL 的<a href="https://dl.acm.org/doi/10.1145/3581784.3607066">论文</a> 里面提到了一个非对称 MPI + OpenMP 并行的技巧，让我想起了我之前的一篇<a href="http://enigmahuang.github.io/files/old-blog-archive/2019-%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7%EF%BC%9A%E9%9D%9E%E5%AF%B9%E7%A7%B0_MPI_OpenMP_%E5%B9%B6%E8%A1%8C.pdf">博文</a>, 想法类似，方法不同。 有一篇非常特立独行的 GB finalist <a href="https://dl.acm.org/doi/10.1145/3581784.3627042">论文</a>，说是在 Cerebras WSE2 上算地震数据，一反以 FLOPS 性能论英雄的常态，说他们跑到了多少多少内存带宽。细看此文，里面实际做的工作不说是设计精巧，简直是大道至简，和其他 GB finalist 放在一起大有一种『我是来刷内存带宽的，你们要干什么』的感觉。</p>

<p>今年 TOP500 前十更新了四名，变化很大，值得单独讨论。先说说 E 级机一血 Frontier. 我在<a href="https://enigmahuang.me/files/old-blog-archive/2022-%E8%B7%A8%E8%BF%87_E_%E7%BA%A7%E8%AE%A1%E7%AE%97%E7%9A%84%E9%97%A8%E6%A7%9B%E4%B9%8B%E5%90%8E.pdf">去年的博文</a>里把 Frontier 称为『强扭的瓜』，今年就看到了两篇论文，分别讨论<a href="https://dl.acm.org/doi/10.1145/3581784.3607089"> Frontier 的整体情况</a>和为 <a href="https://dl.acm.org/doi/10.1145/3581784.3607065"> Frontier 准备软件的情况</a>。这两篇论文我都仔细读了，发现不少有意思的地方：</p>
<ol>
  <li>美国能源部给 E 级机器定 20MW 的功耗限制是这样定出来的：一台超算造价预算 100M USD, 预期服役时间五年，要求五年电费价格不超过造价。同时他们采用一个粗略的估计方式，即 1MW 功耗一年电费 1M USD, 因此推算下来整机功耗不能超过 20MW.</li>
  <li>Frontier 一个 node 有一个 AMD Epyc 7A53 (Zen3) 64-core 处理器和四个 AMD MI250X GPU, 看上去是 1:4 的 CPU:GPU 比例。但是实际上 7A53 有八个 CCD, 一个 MI250X 有两个 GCD, 系统看到一个节点有八个 GPU. 因此他们使用没 1 CCD : 1 GCD 的绑定，每个 CCD 上跑一个 MPI rank. 这是非常传统的做法，这么些年那么多代码说要支持（单进程/单节点）多卡，现在 Frontier 啪的一巴掌给盖回去了。</li>
  <li>Frontier 的 Slingshot NIC 是直接接到 GPU 上的，四个 MI250X 各接一个 NIC. 论文里明确说 “we expect most users will keep their data in the HBM and avoid moving it back and forth to the CPU”.</li>
  <li>尽管 Slingshot 提供 200 GBps endpoint bandwidth, Frontier 测出来的 per NIC bandwidth 主要分布在 3GB/s 到 7GB/s 的范围，少部分能跑到 17.5GB/s. Summit 用的 100 Gbps IB EDR 测出来的 per NIC bandwidth 比较集中在 8.5GB/s 附近。论文指出这是由于 Slingshot 的网络架构特性造成的。</li>
  <li>SHOC benchmark suit 用 AMD 的 HIPIFY 从 CUDA 翻译到 HIP 以后主要只需要修一点语法问题（HIP 用了一些过时的语法或 API），性能 “similar to that of the CUDA version”. 然而这个比较的是 normalized performance, 跟 Summit 上的 V100 比，原始性能对比如何并不清楚。总体来说，论文认为 “porting and optimization to AMD’s HIP was an efficient approach”. 一个侧面佐证是我问了一个 LLNL 的人如何评价 HIP, 他说总体上比较满意，代码翻译过去基本能直接跑，而且性能达到预期。</li>
  <li>GPU-centric programming 吹了几年了，特别是 NVSHMEM 发布以后各家都跟进了 GPU PGAS, 说新时代的并行编程模型要改，要以 GPU 为中心。结果 Frontier 这些人又是一巴掌拍了回去：”Traditional techniques such as HIP/CUDA, OpenMP, and MPI are still valid to achieve high performance on modern supercomputers. In particular, the ‘GPU-Aware MPI + X’ model for inter-node communication remains the predominant narrative for Frontier and the Exascale era.”</li>
  <li>Frontier 上的样板移植程序有不少都是基于网格的计算模式，比如 particle-in-cell, particle mesh, 还有 CFD 的直接网格模拟。比较通用的软件只列出了 GAMESS 和 LAMMPS.</li>
  <li>有些代码属实是过于极端，比如 Pele 这个软件整出来的奥力给：”The unrolled chemistry computation routines can contain upwards of 200k lines of code in a single file, with a single GPU kernel (such as the calculation of a chemical Jacobian) spanning 140k lines of code on its own. These large kernels have been found to use upwards of 18k registers.”</li>
  <li>这一段槽点有点多，直接上原文截图。
<img src="http://enigmahuang.github.io/files/SC23/Frontier_correctness_issues.png" alt="" /></li>
</ol>

<p>现在看起来 Frontier 不是那么跑分机器了。不过在跑分这个问题上，如果我们翻一下历史的合订本，我们会发现 Frontier 的 HPCG 成绩比起一年前没有提高。往好里说，Frontier 的人非常自信，根本不在意更新 HPCG 的跑分。往坏里说，就不知道是谁的锅了。尽管如此，Frontier 还是比 delay no more 了很多次的 Aurora 好。今年 Aurora 终于上榜了，用半部机器的跑分冲到了 TOP500 榜二。不幸的是，Aurora 半部机器的功耗已经超过了 Frontier. 假设 Aurora 把剩下的半台机器开了，性能线性翻倍，那跑到 1 EF 的功耗也要 50MW, 甚至比中国那些拿 7nm 搓出来的机器还要费电。而且 Aurora 还没有提交 HPCG 成绩。考虑到之前已经有<a href="https://chipsandcheese.com/2023/09/23/intels-ponte-vecchio-chiplets-gone-crazy/">文章</a>和<a href="https://www.ixpug.org/images/docs/ISC23/McCalpin_SPR_BW_limits_2023-05-24_final.pdf">报告</a>指出 HBM 版的 SPR 和 PVC 的设计有问题吃不满内存带宽，我觉得牙膏厂这次应该是彻底翻车了。与此同时，Azure 的人<a href="https://www.servethehome.com/microsoft-azure-eagle-is-a-paradigm-shifting-cloud-supercomputer-nvidia-intel/">仅用了三天</a>来调试和测试 HPL 就交了一个接近 Aurora 的成绩，而且 66% HPL 效率也没有比 Frontier 的 71% HPL 效率低多少，甚至还比排第八和第九的同样使用 H100 的 MareNostrum 5 Acc 和 Eos NVIDIA DGX SuperPOD 的 52% 和 64% 要高。</p>

<p>说到功耗，就得顺便说一下国内的小道消息了。国内的三台 E 级机里神威和天河都落地了，只剩下原本应该用海光芯片的机器没了下文。多个方面的消息都证实了那台机器会被一台即将安装于深圳超算中心的华为新机器取代。中科大的安虹老师说海光那台功耗控制不住，如果按原本的技术路线来估计要到 100MW. 这是我此前博文里估计的 40MW 的 2.5 倍。按这个数据倒推，海光应该是拿不出足够的 MI100 同等水平的 DCU 来造新超算。如果按 MI50/MI60 的水平来重新估算功耗，那么确实要接近 100MW 才能达到 1 EF. 更令人好奇的是华为准备拿什么芯片来堆 1 EF. 昇腾加速卡并不支持双精度，华为估计也不太可能单独造一个新的加速卡给这个超算。应该说国内除了 SW26010 系列和 MT2K/MT3K, 我不知道还有哪个加速卡或者芯片能做到 300W 功耗约束下单芯片 15T 以上的双精度性能。 考虑到华为手里还有个 64 核的鲲鹏 920, 要是发一发狠开一开脑洞，造一个 128 core * 2 GHz * SVE 2048 bit + FMA 或者 256 core * 4 GHz * SVE 512 bit + FMA 的处理器，倒是可以摸到双精度 16T 的性能。但是前者呢，NEC Vector Engine 珠玉在前，超长 SIMD 极其容易扑街；后者你相信中芯国际的工艺能造出 256 core @ 4 GHz 还不如相信我是秦始皇。我甚至怀疑 128 core * 2 GHz * SVE 512 bit + FMA 能不能造出来。所以我觉得还是等着看华为重新定义 E 级机比较现实。</p>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="HPC" /><category term="Travel" /><category term="Conference" /><summary type="html"><![CDATA[虽然今年没有中论文，但是机缘巧合，又可以去 SC23 公款旅游了。此时此刻，恰如四年前的彼时彼刻，同样是没有论文，同样是去丹佛。好在今年丹佛天气很好，白天大都是晴天，有十多度接近二十度，也没有下雨下雪什么的。坏在今年丹佛天气真的像天气预报那么好，而我对天气预报的心存疑虑导致我多带了不少衣服。四年过去以后，我感觉丹佛最大的变化就是流浪汉变多了。无论是从机场进市区的铁道途径的地方，还是市区街道转角的小花园，如今都多了不少流浪汉。考虑到我在贝洛伯格也翻了不少垃圾桶，推己及人或许我应该称他们『开拓者』。]]></summary></entry><entry><title type="html">如何在 A64FX 上『正确地』跑 STREAM</title><link href="https://enigmahuang.me/posts/2023/11/A64FX-STREAM/" rel="alternate" type="text/html" title="如何在 A64FX 上『正确地』跑 STREAM" /><published>2023-11-03T00:00:00-07:00</published><updated>2023-11-03T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2023/11/A64FX-STREAM</id><content type="html" xml:base="https://enigmahuang.me/posts/2023/11/A64FX-STREAM/"><![CDATA[<h2 id="缘起">缘起</h2>

<p>富士通的 A64FX 处理器自公开以来就以其 ARM many-core 架构、SVE 向量指令集、1024 GB/s 的大 HBM2 带宽吸引了大量的目光。三年前 Fugaku 登顶 TOP500 以及之后 Fugaku 成为四冠王更是让 A64FX 风头无两。虽然 Knights Landing 已经尝试过把大内存带宽下放到 many-core CPU, 但高达 1024 GB/s 的大内存带宽依旧是 A64FX 的主要卖点之一。2021年初，我们学院买了几台 HPE 的 A64FX 机器，我自然是第一时间就申请了试用。</p>

<p>试了一圈以后我发现坏了，STREAM triad 竟然只能跑到 600+ GB/s, 距离 1024 GB/s 差得有点远啊。幸运的是，这是一个我在 x86 上碰到过的老问题。Triad 的访问模式是 <code class="language-plaintext highlighter-rouge">a[i] = b[i] + scalar * c[i]</code>, 编译器的优化不到位的时候，会先把 <code class="language-plaintext highlighter-rouge">a[i]</code> 的 cache line 读进来，新数据写到 cache line 以后再写回到主内存里去。这样一来，实际上只需要读二写一的操作就变成了读三写一，有四分之一的内存带宽被浪费了。在 x86 上，这个问题可以通过 non-temporal store 解决 [1]。既然如此，那我就继续在 A64FX 上用 non-temporal store 就好了嘛。查了一下，对应的 non-temporal load/store 指令在 SVE intrinsic 里面是 <code class="language-plaintext highlighter-rouge">svldnt1_f64</code> 和 <code class="language-plaintext highlighter-rouge">svstnt1_f64</code> [2]. 换上以后一跑，坏了，还是只有 600+ GB/s.</p>

<p>百思不得其解，然后我开始搜资料，搜到了富士通官方发的一个 PPT [3]. 这里指出，A64FX 需要用 zfill 来告诉系统来避免将 <code class="language-plaintext highlighter-rouge">a[]</code> 数组先从内存读到缓存里。这里放一页 PPT 里的图：</p>

<p><img src="http://enigmahuang.github.io/files/A64FX-STREAM/A64FX-zfill.png" alt="zfill" /></p>

<p>这张图告诉我们，zfill 是通过 <code class="language-plaintext highlighter-rouge">dc zva</code> 这条指令来完成的。那么这条指令如何使用呢？PPT 里没有说。不过 PPT 告诉我们，富士通的编译器专门加了一个 flag 叫 <code class="language-plaintext highlighter-rouge">-Kassume=memory_bandwidth</code>, 加上以后 triad 跑出来的带宽立马从 629 GB/s 飙升到 822 GB/s. 什么叫匠人精神啊，这就是匠人精神！（战术后仰）</p>

<p>当然，除了测内存带宽专用 flag 以外，富士通编译器还有其他的 flag 可以解决这个问题。比如 PPT 里，还给出了这么一组解决方案：<code class="language-plaintext highlighter-rouge">-Kzfill=100 -Kprefetch_sequential=soft -Kprefetch_line=8 -Kprefetch_line_L2=16</code>. 我猜这里 <code class="language-plaintext highlighter-rouge">-Kzfill=100</code> 是告诉编译器 100%  使用 zfill, 就像 ICC 里的 <code class="language-plaintext highlighter-rouge">-qopt-zmm-usage=high</code> 一样。后面其他的预取参数我就先不管了。</p>

<p>那么问题来了，这么好用的富士通编译器哪里有呢？</p>

<h2 id="性空">性空</h2>

<p>没有富士通的编译器那就得自己想办法了。学校的机器既然是 HPE 的，那么自然附带了 Cray 的编译器。Cray 作为一个老牌超算厂商，我们来看看它的自动向量化表现。那么 Cray cc version 10.0.3 给出的答卷是多少分呢？630 GB/s. 好，抬出去，下一个。学校机器上还装过一个 ARM 自己的编译器套件，虽然现在可能因为试用期过了也没有了。知子莫若父，那么 ARM 自己的编译器能交出什么样的答卷呢？很遗憾，还是 600+ GB/s. GCC 呢？21年初的时候我自己编译了一个 GCC 10.2 试了一下，也是只有 620 GB/s 的水平。</p>

<p>自动向量化搞不定，那就只好自己动手写 intrinsic 了。好在富士通的 PPT 给了一个线索：<code class="language-plaintext highlighter-rouge">dc zva</code>. 同时，富士通也给出了一份 triad 的样例汇编代码 [4]，使用了四路循环展开、预取指令和 dc zva。我照猫画虎写出了下面的 C code:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">triad_sve512_unroll4_st</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="n">scalar</span><span class="p">,</span> <span class="kt">double</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span> <span class="k">const</span> <span class="kt">double</span> <span class="o">*</span><span class="n">c</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// a[0 : n0-1] are in an incomplete cache line</span>
    <span class="kt">size_t</span> <span class="n">a_addr</span> <span class="o">=</span> <span class="p">(</span><span class="kt">size_t</span><span class="p">)</span> <span class="n">a</span><span class="p">;</span>
    <span class="kt">size_t</span> <span class="n">a_addr_cl</span> <span class="o">=</span> <span class="p">(</span><span class="n">a_addr</span> <span class="o">+</span> <span class="mi">256</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">256</span> <span class="o">*</span> <span class="mi">256</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">n0</span> <span class="o">=</span> <span class="p">(</span><span class="n">a_addr_cl</span> <span class="o">-</span> <span class="n">a_addr</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">double</span><span class="p">);</span>

    <span class="c1">// a[n0 : n1-1] are in complete cache lines</span>
    <span class="kt">int</span> <span class="n">ncl</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">double</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="n">n0</span><span class="p">)</span> <span class="o">/</span> <span class="mi">256</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">n1</span>  <span class="o">=</span> <span class="n">n0</span> <span class="o">+</span> <span class="n">ncl</span> <span class="o">*</span> <span class="mi">256</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">double</span><span class="p">);</span>

    <span class="c1">// Handle the first part and the last part</span>
    <span class="cp">#pragma omp simd
</span>    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n0</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">scalar</span> <span class="o">*</span> <span class="n">c</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="cp">#pragma omp simd
</span>    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">n1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">scalar</span> <span class="o">*</span> <span class="n">c</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>

    <span class="c1">// Handle complete cache lines</span>
    <span class="n">a</span> <span class="o">+=</span> <span class="n">n0</span><span class="p">;</span>  <span class="n">b</span> <span class="o">+=</span> <span class="n">n0</span><span class="p">;</span>  <span class="n">c</span> <span class="o">+=</span> <span class="n">n0</span><span class="p">;</span>
    <span class="n">svbool_t</span> <span class="n">ptrue64b</span> <span class="o">=</span> <span class="n">svptrue_b64</span><span class="p">();</span>
    <span class="n">svfloat64_t</span> <span class="n">vec_scalar</span> <span class="o">=</span> <span class="n">svdup_f64_z</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">scalar</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">icl</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">icl</span> <span class="o">&lt;</span> <span class="n">ncl</span><span class="p">;</span> <span class="n">icl</span><span class="o">++</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">//asm("dc zva, %[a_ptr]\n" : : [a_ptr] "r" (a) : "memory");</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_b0</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">0</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_b1</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">1</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_b2</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_b3</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">3</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_c0</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">c</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">0</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_c1</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">c</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">1</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_c2</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">c</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_c3</span> <span class="o">=</span> <span class="n">svld1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">c</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">3</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_a0</span> <span class="o">=</span> <span class="n">svmad_f64_z</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">vec_scalar</span><span class="p">,</span> <span class="n">vec_c0</span><span class="p">,</span> <span class="n">vec_b0</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_a1</span> <span class="o">=</span> <span class="n">svmad_f64_z</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">vec_scalar</span><span class="p">,</span> <span class="n">vec_c1</span><span class="p">,</span> <span class="n">vec_b1</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_a2</span> <span class="o">=</span> <span class="n">svmad_f64_z</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">vec_scalar</span><span class="p">,</span> <span class="n">vec_c2</span><span class="p">,</span> <span class="n">vec_b2</span><span class="p">);</span>
        <span class="n">svfloat64_t</span> <span class="n">vec_a3</span> <span class="o">=</span> <span class="n">svmad_f64_z</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">vec_scalar</span><span class="p">,</span> <span class="n">vec_c3</span><span class="p">,</span> <span class="n">vec_b3</span><span class="p">);</span>
        <span class="n">svst1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">a</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">0</span><span class="p">,</span> <span class="n">vec_a0</span><span class="p">);</span>
        <span class="n">svst1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">a</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">1</span><span class="p">,</span> <span class="n">vec_a1</span><span class="p">);</span>
        <span class="n">svst1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">a</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">vec_a2</span><span class="p">);</span>
        <span class="n">svst1</span><span class="p">(</span><span class="n">ptrue64b</span><span class="p">,</span> <span class="n">a</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="mi">3</span><span class="p">,</span> <span class="n">vec_a3</span><span class="p">);</span>
        <span class="n">a</span> <span class="o">+=</span> <span class="mi">32</span><span class="p">;</span>  <span class="n">b</span> <span class="o">+=</span> <span class="mi">32</span><span class="p">;</span>  <span class="n">c</span> <span class="o">+=</span> <span class="mi">32</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这个代码是单线程 kernel, 需要在外面的多线程区域内调用。第 26 行被注释掉了，跑出来的速度是 630 GB/s. 一旦打开第 26 行，速度立即下降到了 286 GB/s. 说实话，我也不确定怎么用 <code class="language-plaintext highlighter-rouge">dc zva</code> 才是正确的。于是我给富士通的工程师发了一封邮件，对面热情地回复了我：</p>

<blockquote>
  <p>You need to write your C code so that your gcc -S output is the same as the GitHub code.</p>
</blockquote>

<p>泪流满面，日本的匠人精神实在是太棒了！</p>

<h2 id="此即诞生之刻">此即诞生之刻</h2>

<p>由于某些机缘巧合，我又重新捡起这个问题来看了。重新搜了一圈以后发现了一个 PPT [5]. 这个 PPT 仿照了一个 ARM 官方参考代码 [6] 里的的做法，为 zfill 加了一个 100 * 256 bytes 的提前量，其中 256 bytes 是 cache line size. 代码中的注释指出：</p>

<blockquote>
  <p>The zfill distance must be large enough to be ahead of the L2 prefetcher.</p>
</blockquote>

<p>但是我查了一圈没有查到 L2 prefetcher 的预取距离有多大。我在<a href="http://enigmahuang.github.io/files/A64FX-STREAM/my_triad.c">自己的代码</a>上面试了一下，发现最小只要提前 12 个 cache lines 进行预取就可以跑到 800-820 GB/s 左右的速度。</p>

<h2 id="references">References</h2>

<ol>
  <li>
    <p><a href="https://sites.utexas.edu/jdm4372/2018/01/01/notes-on-non-temporal-aka-streaming-stores/">Notes on “non-temporal” (aka “streaming”) stores</a></p>
  </li>
  <li>
    <p><a href="https://developer.arm.com/architectures/instruction-sets/intrinsics/#q=ldnt">ARM Intrinsics</a></p>
  </li>
  <li>
    <p><a href="https://community.arm.com/developer/research/m/resources/993/download">Performance Optimization of SVE Enabled Arm Processor A64FX using Fujitsu Compiler</a></p>
  </li>
  <li>
    <p><a href="https://github.com/fujitsu/A64FX/blob/master/sample/stream.kernel.S">GitHub - fujitsu/A64FX sample</a></p>
  </li>
  <li>
    <p><a href="https://www.stonybrook.edu/commcms/ookami/_pdf/Gosh_UGM2023.pdf">Impact of Write-Allocate Elimination for Graph Analytics on Ookami</a></p>
  </li>
  <li>
    <p><a href="https://gitlab.com/arm-hpc/training/arm-sve-tools/-/blob/master/06_A64FX/02_stream/04_stream_zfill/kernel-zfill.c">ARM HPC Resources - training - arm-sve-tools</a></p>
  </li>
</ol>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><category term="ARM" /><category term="SIMD" /><category term="cache" /><summary type="html"><![CDATA[缘起]]></summary></entry><entry><title type="html">历史博文存档</title><link href="https://enigmahuang.me/posts/2023/10/old-blog-archive/" rel="alternate" type="text/html" title="历史博文存档" /><published>2023-10-15T00:00:00-07:00</published><updated>2023-10-15T00:00:00-07:00</updated><id>https://enigmahuang.me/posts/2023/10/old-blog-archive</id><content type="html" xml:base="https://enigmahuang.me/posts/2023/10/old-blog-archive/"><![CDATA[<p>以前的博文我懒得逐个重新手打 markdown 了，以网页打印 PDF 的方式提供部分博文的存档，以时间倒序排列：</p>

<h3 id="2023">2023</h3>

<ul>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2023-记一次笔记本尸体拯救.pdf">记一次笔记本尸体拯救</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2023-稠密对称矩阵特征值求解器的数学与算法.pdf">稠密对称矩阵特征值求解器的数学与算法</a></p>
  </li>
</ul>

<h3 id="2022">2022</h3>

<ul>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-SC22：年轻人的第一次故地重游.pdf">SC22：年轻人的第一次故地重游</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-无内鬼，来点须弥笑话.pdf">无内鬼，来点须弥笑话</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-跨过_E_级计算的门槛之后.pdf">跨过 E 级计算的门槛之后</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-Jack_Dongarra_的图灵奖与并行数值线性代数库之路.pdf">Jack Dongarra 的图灵奖与并行数值线性代数库之路</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-COSMA_算法分析.pdf">COSMA 算法分析</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2022-ARM_ASIMD_SVE_向量数学库接口.pdf">ARM ASIMD &amp; SVE 向量数学库接口</a></p>
  </li>
</ul>

<h3 id="2021">2021</h3>

<ul>
  <li><a href="http://enigmahuang.github.io/files/old-blog-archive/2021-【提瓦特测绘局】天空岛高度估算.pdf">【提瓦特测绘局】天空岛高度估算</a></li>
</ul>

<h3 id="2020">2020</h3>

<ul>
  <li><a href="http://enigmahuang.github.io/files/old-blog-archive/2020-分离编译并打包_CUDA_函数库.pdf">分离编译并打包 CUDA 函数库</a></li>
</ul>

<h3 id="2019">2019</h3>

<ul>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2019-SC19：年轻人的第一次公费旅游.pdf">SC19：年轻人的第一次公费旅游</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2019-奇技淫巧：非对称_MPI_OpenMP_并行.pdf">奇技淫巧：非对称 MPI + OpenMP 并行</a></p>
  </li>
</ul>

<h3 id="2018">2018</h3>

<ul>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2018-那些年数值计算教会我的事.pdf">那些年数值计算教会我的事</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2018-2D_Poisson_方程与_Finite_Element_Method.pdf">2D Poisson 方程与 Finite Element Method</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2018-代数多重网格（Algebraic_Multigrid）简介.pdf">代数多重网格（Algebraic Multigrid）简介</a></p>
  </li>
</ul>

<h3 id="2017">2017</h3>

<ul>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2017-HPCG_3_reference_implementation_阅读笔记.pdf">HPCG 3.0 reference implementation 阅读笔记</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2017-泊松方程的中心差分格式与多重网格法.pdf">泊松方程的中心差分格式与多重网格法</a></p>
  </li>
  <li>
    <p><a href="http://enigmahuang.github.io/files/old-blog-archive/2017-使用_AVX_系列指令集进行向量化.pdf">使用 AVX 系列指令集进行向量化</a></p>
  </li>
</ul>]]></content><author><name>Rainmaker&apos;s Notebook</name></author><summary type="html"><![CDATA[以前的博文我懒得逐个重新手打 markdown 了，以网页打印 PDF 的方式提供部分博文的存档，以时间倒序排列：]]></summary></entry></feed>