<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://jinmoza.github.io/</id>
    <title>Jin's Notes</title>
    <updated>2026-11-01T14:23:21.305Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>jinmoza</name>
        <uri>https://jinmoza.github.io/</uri>
    </author>
    <link rel="alternate" href="https://jinmoza.github.io/"/>
    <link rel="self" href="https://jinmoza.github.io/atom.xml"/>
    <subtitle>一个关于系统性能、编译器、AI 工程与个人成长的技术博客。</subtitle>
    <rights>Copyright © 2026 jinmoza</rights>
    <entry>
        <title type="html"><![CDATA[LLVM 236362：AMDGPU F8、F6、F4 Matrix Intrinsic 过宽操作数修复方案]]></title>
        <id>https://jinmoza.github.io/posts/llvm-amdgpu-f8f6f4-overwide-operands/</id>
        <link href="https://jinmoza.github.io/posts/llvm-amdgpu-f8f6f4-overwide-operands/"/>
        <updated>2026-11-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[修复 LLVM AMDGPU 后端在 -O0 下为 F8/F6/F4 WMMA、MFMA 生成过宽寄存器 tuple，导致文本汇编无法重新汇编的问题。]]></summary>
        <content type="html"><![CDATA[<h2>项目摘要</h2>
<p>这项工作修复了 LLVM AMDGPU 后端在 <code>-O0</code> 下处理 F8/F6/F4 WMMA/MFMA intrinsic 时的一类正确性问题：Clang 和合法 LLVM IR 可以使用“最大宽度”的向量类型承载矩阵输入，而运行时 format 实际只消费其中一个低位前缀。修复前，SelectionDAG 和 GlobalISel 都会把向量类型的完整宽度带入机器操作数，导致 <code>llc</code> 输出的寄存器 tuple 与 format 冲突，生成的文本汇编无法被 <code>llvm-mc</code> 重新汇编。</p>
<p>最终方案在两套指令选择器分流之前，于 <code>AMDGPULowerIntrinsics</code> 中增加共享的 pre-isel 规范化：分别读取矩阵 A、B 的 format，复用 AMDGPU 现有的 format-to-register-count 映射，只保留硬件真正读取的低位元素，并以精确的 source overload 重建同一个 intrinsic。这样既保持了现有 IR 的兼容性，也让寄存器分配、依赖分析、汇编打印和机器编码看到一致的操作数范围。</p>
<ul>
<li>上游 Issue：<a href="https://github.com/llvm/llvm-project/issues/236362">llvm/llvm-project#236362</a></li>
<li>上游 PR：<a href="https://github.com/llvm/llvm-project/pull/237722">llvm/llvm-project#237722</a></li>
<li>生产代码与完整补丁：<a href="https://github.com/llvm/llvm-project/pull/237722/files">PR #237722 Files</a></li>
</ul>
<h2>1. 问题为什么真实存在</h2>
<h3>1.1 一个 immediate format 同时决定数据格式和寄存器宽度</h3>
<p>gfx1250 WMMA 与 gfx950 MFMA 的低精度矩阵指令使用 format 参数描述 A、B 的元素格式。不同格式对应不同的硬件 source tuple 宽度：</p>
<table>
<thead>
<tr>
<th>元素格式</th>
<th>format 值</th>
<th>WMMA 每个 source</th>
<th>MFMA 每个 source</th>
</tr>
</thead>
<tbody>
<tr>
<td>FP8 / BF8</td>
<td>0 / 1</td>
<td>16 × i32</td>
<td>8 × i32</td>
</tr>
<tr>
<td>FP6 / BF6</td>
<td>2 / 3</td>
<td>12 × i32</td>
<td>6 × i32</td>
</tr>
<tr>
<td>FP4</td>
<td>4</td>
<td>8 × i32</td>
<td>4 × i32</td>
</tr>
</tbody>
</table>
<p>WMMA 和 MFMA 对 0..4 的枚举命名不同：WMMA 使用 <code>MATRIX_FMT_*</code>，MFMA 使用 <code>FP8_E4M3</code>、<code>FP8_E5M2</code>、<code>FP6_E2M3</code>、<code>FP6_E3M2</code>、<code>FP4_E2M1</code>。这里的数字只是两组相同顺序的 format 枚举，不代表向量长度或 scale 值。</p>
<p>宽度映射已经存在于 <code>AMDGPUBaseInfo</code>：</p>
<ul>
<li><code>wmmaScaleF8F6F4FormatToNumRegs()</code> 返回 16、12 或 8；</li>
<li><code>mfmaScaleF8F6F4FormatToNumRegs()</code> 返回 8、6 或 4。</li>
</ul>
<p>因此本次修复不应该再维护一份 format switch，而应复用后端已有的唯一映射。</p>
<h3>1.2 Clang <code>-O0</code> 会走到这条路径</h3>
<p>这不是只靠手写 IR 才能触发的边界情况。当前 Clang builtin 的公开签名使用最大 source overload：</p>
<ul>
<li>三类 WMMA builtin 的 A、B 都是 <code>&lt;16 x i32&gt;</code>；</li>
<li>两类 MFMA builtin 的 A、B 都是 <code>&lt;8 x i32&gt;</code>。</li>
</ul>
<p>当用户选择 FP6、BF6 或 FP4 时，向量容器会比 format 真正需要的宽。显式检查 <code>clang -O0 -emit-llvm</code> 和禁用后续 LLVM passes 的原始前端 IR，都能观察到这种最大 overload。</p>
<p>在优化流水线中，AMDGPU InstCombine 已经会插入低位 <code>shufflevector</code>，提前把 source 收缩成精确 overload，所以 <code>-O1</code> 及以上可能掩盖问题。这里必须区分两个概念：是 Clang/<code>opt</code> 的 IR 优化流水线做了规范化，而不是单独把 <code>llc</code> 的 CodeGen 优化级别设为 <code>-O2</code> 就必然会运行 InstCombine。</p>
<h3>1.3 Verifier 有意接受“至少足够宽”</h3>
<p>AMDGPU verifier 的契约是：</p>
<pre><code>actual source width &gt;= width required by format
</code></pre>
<p>例如，FP4 WMMA 只需要 <code>&lt;8 x i32&gt;</code>，但 <code>&lt;16 x i32&gt;</code> 仍是合法 IR。这个契约使已有的最大 overload producer 保持兼容，也给优化阶段留下规范化空间。因此不能把过宽输入简单归类为非法 IR，更不能只通过收紧 verifier 来“修复” CodeGen。</p>
<h3>1.4 指令选择、汇编打印和机器编码看到了不同的真相</h3>
<p>修复前的关键路径是：</p>
<pre><code>Clang -O0 / 合法未优化 IR
       │
       │  &lt;16 x i32&gt; + FP4（WMMA）
       ▼
AMDGPU verifier：合法，因为 16 &gt;= 8
       │
       ▼
SelectionDAG / GlobalISel
按 overloaded vector type 保留 16-register source
       │
       ▼
llc 打印 v[0:15]，同时标注 MATRIX_FMT_FP4
       │
       ▼
llvm-mc：FP4 只能使用 8-register tuple，拒绝汇编
</code></pre>
<p>代表性的错误汇编是：</p>
<pre><code>v_wmma_f32_16x16x128_f8f6f4 \
  v[32:39], v[0:15], v[16:31], v[32:39] \
  matrix_a_fmt:MATRIX_FMT_FP4 matrix_b_fmt:MATRIX_FMT_FP4
</code></pre>
<p><code>llvm-mc</code> 报告：</p>
<pre><code>error: wrong register tuple size for MATRIX_FMT_FP4
</code></pre>
<p>MFMA 的表现相同：FP4 指令被打印为 8-register A/B tuple，但 assembler 只允许 4-register tuple。</p>
<p>这个问题有一个容易误导调查的现象：直接执行 <code>llc -filetype=obj</code> 可以成功。原因是机器编码记录 source 的起始寄存器和 format，反汇编器再根据 format 恢复正确的结束寄存器；文本汇编则把 tuple 的起止位置都显式写出来，所以能暴露二者矛盾。直接 object 成功并不说明内部状态正确，因为寄存器分配、hazard recognizer 和 waitcnt 分析仍然把未被硬件读取的高位寄存器当成真实依赖。</p>
<h2>2. 为什么问题必须同时覆盖两套指令选择器</h2>
<p>AMDGPU 当前同时存在 SelectionDAG 和 GlobalISel：</p>
<pre><code>LLVM IR
   │
   ├── SelectionDAG：成熟的旧路径
   │
   └── GlobalISel：基于通用 MIR 的新路径
</code></pre>
<p>使用同一个最小输入分别运行：</p>
<pre><code>llc -O0 -global-isel=0 ...
llc -O0 -global-isel=1 -global-isel-abort=1 ...
</code></pre>
<p>两条路径都会生成过宽 tuple。<code>-global-isel-abort=1</code> 很重要，它禁止 GlobalISel 失败后回退到 SelectionDAG；若使用允许 fallback 的模式，即使测试通过，也不能证明 GlobalISel 自身正确。</p>
<p>因此，分别在两个 selector 中修同一问题会产生重复逻辑和长期漂移风险。更合理的边界是在二者分流前建立同一份 canonical IR。</p>
<h2>3. 范围分析：为什么是五类 intrinsic</h2>
<p>对 TableGen、verifier、InstCombine、SelectionDAG/GlobalISel 引用以及 Clang builtin 做完整枚举后，具有独立 A/B runtime format、并存在同一过宽契约的 intrinsic 恰好有五类：</p>
<ol>
<li><code>amdgcn_wmma_f32_16x16x128_f8f6f4</code></li>
<li><code>amdgcn_wmma_scale_f32_16x16x128_f8f6f4</code></li>
<li><code>amdgcn_wmma_scale16_f32_16x16x128_f8f6f4</code></li>
<li><code>amdgcn_mfma_scale_f32_16x16x128_f8f6f4</code></li>
<li><code>amdgcn_mfma_scale_f32_32x32x64_f8f6f4</code></li>
</ol>
<p>五类都在未修复基准上用 SelectionDAG 和禁止 fallback 的 GlobalISel 直接复现过，不是从第一个 WMMA case 推测出来的预防性覆盖。</p>
<p>这里的 <code>scaled</code> 表示矩阵运算还携带 A/B 的 scale 信息，不是“缩小向量”。WMMA <code>scale</code> 与 <code>scale16</code> 的主要区别是 scale source 类型分别包含 i32 与 i64 形式；它们的 matrix format 和 source tuple 规则相同。MFMA 在这组 runtime-format intrinsic 中只有 scaled 形式。</p>
<h3>strict gfx1250 的特殊顺序</h3>
<p>在 strict gfx1250 上，普通 unscaled WMMA 会在同一个 pre-isel pass 中转换成 scale exponent 为 0 的 scaled WMMA，相当于中性缩放。但普通 WMMA 仍是独立的 IR intrinsic 入口，不能因为最终变成 scaled 形式就省略覆盖。</p>
<p>正确顺序必须是：</p>
<pre><code>unscaled WMMA
     │
     ▼
先按 format 规范化 source overload
     │
     ▼
再执行既有 unscaled -&gt; neutral-scaled 转换
</code></pre>
<p>如果先删除旧调用、再尝试窄化，或者 normalization 之后仍把旧指针交给既有 visitor，就会产生 use-after-erase 风险；如果完全跳过普通入口，过宽 overload 又会被原样带进 scaled WMMA。</p>
<h2>4. 方案选型</h2>
<h3>4.1 最终选择：<code>AMDGPULowerIntrinsics</code></h3>
<p><code>AMDGPULowerIntrinsics</code> 的文件级职责就是处理“否则必须在 SelectionDAG 和 GlobalISel 中分别实现”的 AMDGPU intrinsic lowering。它具备四个关键条件：</p>
<ol>
<li><strong><code>-O0</code> 也运行。</strong> 它是 CodeGen 正确性路径，不依赖 IR 优化级别。</li>
<li><strong>位于两套 selector 之前。</strong> 一次规范化同时覆盖 SelectionDAG 和 GlobalISel。</li>
<li><strong>位于寄存器分配之前。</strong> 后续 RA、hazard 和 waitcnt 从一开始就看到硬件真实读取的范围。</li>
<li><strong>现有基础设施已经完整。</strong> TableGen 已有不同宽度和 mixed-format 的指令变体，BaseInfo 已有 format-to-width 映射；只需把 intrinsic 类型整理正确。</li>
</ol>
<p>整体位置如下：</p>
<pre><code>合法但未规范化的 LLVM IR
            │
            ▼
AMDGPULowerIntrinsics      ← 本次修改
            │
            ├── SelectionDAG
            └── GlobalISel
                    │
                    ▼
             MachineInstr / RA
                    │
                    ▼
              AsmPrinter / MC
</code></pre>
<h3>4.2 被否决的备选方案</h3>
<h4>在 <code>-O0</code> 启用 InstCombine</h4>
<p>现有 AMDGPU InstCombine 的确已有同构窄化，但它只是完整 InstCombine 中的一条 target hook。为一个后端正确性问题在所有 O0 编译中启用完整 InstCombine，会同时触发大量常量折叠、代数化简、cast 合并、比较重写和 DCE，改变 O0 的调试与编译行为，范围明显过大。</p>
<h4>只修改 Clang</h4>
<p>Clang 不是 LLVM IR 的唯一 producer。Verifier 已经承诺过宽 IR 合法，外部前端、旧 bitcode 和手写 IR 都可以进入后端，所以后端不能把正确性依赖于某个前端提前窄化。</p>
<h4>分别修改 SelectionDAG 和 GlobalISel</h4>
<p>这会复制 format 映射、参数索引和重建规则，既增加实现量，也容易让两套 selector 的覆盖发生漂移。</p>
<h4>在 AsmPrinter 或 MC 层截断 tuple</h4>
<p>它可以让文本汇编表面上通过，但修复发生在 RA、hazard 和 waitcnt 之后，无法纠正内部过宽依赖，也会把一个 CodeGen 建模错误伪装成打印问题。</p>
<h4>收紧 verifier 或 intrinsic 签名</h4>
<p>这会拒绝当前 Clang O0 正常生成、且现有契约明确允许的 IR，构成兼容性变化；要这样做还必须同步修改 Clang 和其他 producer，不适合作为当前 bug 的最小修复。</p>
<h2>5. 最终算法</h2>
<h3>5.1 统一 helper，显式分发</h3>
<p>实现增加一个本地 helper：</p>
<pre><code>narrowF8F6F4MatrixOperands(IntrinsicInst &amp;)
    -&gt; { Current IntrinsicInst*, Changed }
</code></pre>
<p><code>run()</code> 显式列出五个 intrinsic ID，而不是按名字模糊匹配。显式 switch 让当前范围可审计；未来如果新增第六类 runtime-format intrinsic，必须同时补充分发和测试，而不会被未经验证的“通用规则”静默纳入。</p>
<p>两类签名的参数布局不同：</p>
<table>
<thead>
<tr>
<th>family</th>
<th>source A/B</th>
<th>format A/B</th>
<th>overload 类型列表</th>
</tr>
</thead>
<tbody>
<tr>
<td>WMMA</td>
<td>参数 1 / 3</td>
<td>参数 0 / 2</td>
<td>返回类型、A 类型、B 类型</td>
</tr>
<tr>
<td>MFMA</td>
<td>参数 0 / 1</td>
<td>参数 3 / 4</td>
<td>A 类型、B 类型</td>
</tr>
</tbody>
</table>
<p>因此方案共享的是上层算法，而不是假设 WMMA/MFMA 有相同签名。helper 内先通过 family switch 取得索引和 overload 规则，再执行统一逻辑。</p>
<h3>5.2 A、B 必须独立计算</h3>
<p>对每个 source 分别执行：</p>
<pre><code>读取自己的 format
    │
    ▼
调用对应 family 的 format-to-width helper
    │
    ▼
Actual == Required：保留原 Value
Actual &gt;  Required：保留 [0, Required) 低位前缀
</code></pre>
<p>不能用 A 的 format 同时决定 B，也不能要求“两侧都过宽”才触发。真实 Clang O0 形状经常正是 mixed-format 加单侧过宽，例如：</p>
<ul>
<li>WMMA：A=BF8，<code>&lt;16 x i32&gt;</code> 已精确；B=FP6，<code>&lt;16 x i32&gt;</code> 需要窄到 12；</li>
<li>MFMA：A=BF6，<code>&lt;8 x i32&gt;</code> 需要窄到 6；B=BF8，<code>&lt;8 x i32&gt;</code> 已精确。</li>
</ul>
<p>这两个 case 同时验证 A/B format 独立、两种单侧方向以及 FP6/BF6 的中间宽度，而不需要把所有组合机械展开成测试矩阵。</p>
<h3>5.3 先完整验证，再修改</h3>
<p>合法输入应满足：</p>
<ul>
<li>format 是 <code>ConstantInt</code>，并位于对应 family 的 0..4 枚举范围；</li>
<li>source 是 i32 <code>FixedVectorType</code>；</li>
<li>WMMA overload 宽度属于 8/12/16，MFMA 属于 4/6/8；</li>
<li>两侧实际宽度都不小于各自 format 的需求。</li>
</ul>
<p>helper 在产生任何新 IR 前先验证两侧。如果任一侧是 malformed IR，则整个 helper no-op，把诊断和拒绝继续交给现有 verifier/selector；它不会只修另一侧、补零、猜测 format，或把非法输入静默洗成合法 IR。这个 release-build 防御也避免把越界 MFMA format 错当成 FP8。</p>
<h3>5.4 使用 identity-prefix <code>shufflevector</code></h3>
<p>过宽 source 通过 mask <code>[0, 1, ..., Required-1]</code> 生成低位前缀：</p>
<pre><code>%narrow = shufflevector &lt;16 x i32&gt; %src, poison,
                        &lt;8 x i32&gt; &lt;0, 1, 2, 3, 4, 5, 6, 7&gt;
</code></pre>
<p>方案早期曾考虑使用 <code>llvm.vector.extract</code> 表示连续低位提取。针对候选实现的正向测试发现，AMDGPU GlobalISel 会在该表示上因无法合法化而 abort，而等价的 identity-prefix <code>shufflevector</code> 可以由 SelectionDAG 和 GlobalISel 都稳定处理。因此最终实现选择 <code>shufflevector</code>。这个修正体现了一个重要原则：IR 语义等价不等于目标 legalization 能力等价，最终表示必须以两条真实 selector pipeline 验证。</p>
<p>测试不仅检查“结果宽了还是窄了”，还绑定 mask 的非零后续元素，确保实现取得的是低位 <code>[0, N)</code>，而不是任意 N 个元素或高位切片。</p>
<h3>5.5 重建 overloaded intrinsic</h3>
<p>LLVM intrinsic declaration 的函数类型已经编码 source overload，因此不能对旧调用直接 <code>setArgOperand</code> 塞入不同向量类型。只要任一 source 发生变化，就必须：</p>
<ol>
<li>复制完整实参数组，只替换需要窄化的 source；</li>
<li>按 WMMA/MFMA 各自的 overload 列表创建同 ID 新调用；</li>
<li>保留 debug location、metadata、attributes、fast-math flags、operand bundles、tail-call kind、calling convention 和 SSA 名称；</li>
<li><code>replaceAllUsesWith(NewI)</code> 后删除旧调用；</li>
<li>返回新的 <code>Current</code> 指针，供普通 WMMA 的 strict lowering 继续使用。</li>
</ol>
<p>若 A/B 都已是精确宽度，helper 直接返回原调用和 <code>Changed=false</code>，不创建冗余 shuffle 或 declaration。这也保证新 overload 即使稍后再次被 module 遍历，也会幂等 no-op。</p>
<h2>6. 为什么截掉高位元素保持语义</h2>
<p>以 FP4 WMMA 为例：</p>
<pre><code>原容器：a[0] ... a[7] | a[8] ... a[15]
硬件读取：a[0] ... a[7]
修复保留：a[0] ... a[7]
</code></pre>
<p>等价性有三层独立依据：</p>
<ol>
<li><code>AMDGPUBaseInfo</code> 和 MC 校验共同定义了每个 format 的真实寄存器宽度；</li>
<li>现有 AMDGPU InstCombine 已经对同五类 intrinsic 做相同的低位前缀提取；</li>
<li>修复前直接生成 object 后的反汇编本来就按 format 显示窄 tuple，说明高位没有进入最终机器编码。</li>
</ol>
<p>因此本次变换没有定义新的数学语义，只是把“format 已经隐含决定的有效前缀”显式反映到 overloaded source 类型中。</p>
<p>保持不变的内容包括：intrinsic ID、format、累加器、scale/reuse 等其他参数、结果类型、数值计算和内存行为。允许变化的是内部 IR 形状、寄存器编号、spill/依赖指令以及最终 object 字节布局；因为 RA 现在看到了更准确的 live range，所以不要求修复前后 object 完全相同。</p>
<h2>7. 边界问题与范围控制</h2>
<h3>7.1 mixed-format</h3>
<p>A、B 可以选择不同格式，例如 WMMA A=FP4、B=FP8。两侧需要的 tuple 分别是 8 和 16，不能共享一个宽度。mixed-format 测试的价值不是新增格式组合，而是证明实现真正独立读取了两个 format。</p>
<h3>7.2 仅一侧过宽</h3>
<p>另一侧已经精确时，不应为了“统一”再创建无意义的 shuffle。新 intrinsic 的 overload 必须组合“窄化后的一侧类型”和“原有精确类型”。</p>
<h3>7.3 FP6/BF6 中间档</h3>
<p>只测试 FP4 会留下一个漏洞：错误实现可能把所有非 FP8 输入都统一缩到最小宽度。WMMA 12-register 与 MFMA 6-register case 证明代码复用了完整 format 映射，而不是只实现了“最大到最小”。</p>
<h3>7.4 固定 FP4 的三个相邻 intrinsic</h3>
<p>另外三个 <code>32x16x128.f4</code> WMMA intrinsic 的 format 固定为 FP4，没有 runtime A/B format。手写非规范 overload 可以通过当前 verifier，但会在 SelectionDAG/GlobalISel 直接 <code>Cannot select</code>，而不是先生成汇编再被 <code>llvm-mc</code> 拒绝。</p>
<p>它们没有被并入本次 helper，原因是：</p>
<ul>
<li>当前 Clang O0 builtin 已生成精确的 A <code>&lt;16 x i32&gt;</code>、B <code>&lt;8 x i32&gt;</code>，正常生产路径不可达该过宽形状；</li>
<li>没有 runtime format 可作为“应该保留多少元素”的动态语义依据；</li>
<li>更合理的修复方向可能是 verifier 精确约束和更早诊断，需要单独枚举异常形状与兼容性影响。</li>
</ul>
<p>这不是忽略已知问题，而是区分“当前生产路径的 CodeGen 正确性 bug”和“手写 IR 的 verifier 契约缺口”，避免把不同根因打包进同一个上游 patch。</p>
<h3>7.5 MFMA verifier 的诊断参数问题</h3>
<p>调查还发现，MFMA Src1 宽度判定正确读取参数 4 的 B format，但失败诊断把参数 5 的 A scale selector 当作上下文打印。它只影响错误信息，不改变接受条件或本次 lowering，因此被记录为独立的小修复，没有顺手混入 #236362。</p>
<h2>8. 测试策略：证明修复的是完整契约</h2>
<h3>8.1 RED 基线</h3>
<p>修复前，五类 intrinsic 都在以下两条 pipeline 中生成过宽 tuple，并被 <code>llvm-mc</code> 拒绝：</p>
<pre><code>llc -O0 -global-isel=0 | llvm-mc
llc -O0 -global-isel=1 -global-isel-abort=1 | llvm-mc
</code></pre>
<p>额外基线覆盖 mixed-format、A/B 两种单侧过宽方向以及 FP6/BF6 中间宽度，避免只修 issue 最初展示的对称 FP4 WMMA。</p>
<h3>8.2 两份聚焦回归测试</h3>
<ul>
<li>WMMA overwide：<code>llvm/test/CodeGen/AMDGPU/llvm.amdgcn.wmma.f8f6f4.overwide.ll</code></li>
<li>MFMA overwide：<code>llvm/test/CodeGen/AMDGPU/llvm.amdgcn.mfma.scale.f8f6f4.overwide.ll</code></li>
</ul>
<p>测试同时锁定：</p>
<ul>
<li>3 个 WMMA 与 2 个 MFMA intrinsic 的显式分发；</li>
<li>SelectionDAG 与禁止 fallback 的 GlobalISel；</li>
<li>FP4 最窄宽度和 FP6/BF6 中间宽度；</li>
<li>mixed-format 与仅一侧窄化；</li>
<li>低位 prefix 的具体来源和新 intrinsic 的精确 overload；</li>
<li>gfx1250 non-strict 与 strict 模式；</li>
<li>strict 模式下 unscaled WMMA 转为 scaled WMMA，且两侧 neutral scale exponent 均为 0；</li>
<li><code>.ll -&gt; llc -&gt; .s -&gt; llvm-mc -&gt; .o</code> 的真实文本汇编闭环。</li>
</ul>
<p>FileCheck 用于观察 opcode、format、IR mask 和 overload；真正强制 tuple 宽度的是 <code>llvm-mc</code>。仅检查 <code>llc</code> 退出码或使用宽松正则匹配 opcode，都无法证明输出汇编能够重新组装。</p>
<h3>8.3 既有生成测试与全量回归</h3>
<p>规范化改变了合法过宽输入的 tuple、寄存器分配、load/wait 调度和部分 scratch register 选择，因此四份既有自动生成 CodeGen 测试需要按原脚本重新生成并人工审查，而不能只新增专项测试后忽略旧 CHECK 失败。</p>
<p>最终验证结果：</p>
<ul>
<li>8 个相关 verifier、InstCombine 与 CodeGen 测试全部通过；</li>
<li>WMMA FP4、FP6、精确 FP8，以及 MFMA FP4 的真实 object/disassembly round-trip 通过；</li>
<li><code>check-llvm-codegen-amdgpu</code> 共发现 5243 项：5236 通过、2 unsupported、5 expected failure、0 unexpected failure；</li>
<li><code>git diff --check</code> 与 <code>git clang-format</code> 通过；</li>
<li>PR 仅包含一个 production 源文件、四个更新后的生成测试和两个新增聚焦测试。</li>
</ul>
<h2>9. 结果与工程价值</h2>
<p>修复后的 WMMA FP4 汇编变为：</p>
<pre><code>v_wmma_f32_16x16x128_f8f6f4 \
  v[8:15], v[0:7], v[16:23], v[8:15] \
  matrix_a_fmt:MATRIX_FMT_FP4 matrix_b_fmt:MATRIX_FMT_FP4
</code></pre>
<p>MFMA FP4 source tuple 则从 <code>v[0:7]</code> / <code>v[8:15]</code> 收缩为 <code>v[0:3]</code> / <code>v[8:11]</code>。<code>llc</code> 的文本输出现在与 MC 校验和硬件编码一致。</p>
<p>在一个 WMMA 最小例中，静态机器码观察到声明的 VGPR 数从 40 降到 24，<code>scratch_load_b32</code> 从 9 条降到 8 条；MFMA 代表例的总 VGPR 数不变，但 source tuple 已被修正。这些结果说明更准确的依赖模型可能减少寄存器压力和保守操作，但没有真机 benchmark、profiling 或 PMU 数据，因此本项目只主张正确性修复和静态资源改善，不声称运行性能提升。</p>
]]></content>
        <author>
            <name>jinmoza</name>
            <uri>https://jinmoza.github.io/</uri>
        </author>
        <published>2026-11-01T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[LLVM AMDGPU：修复 MFMA scale verifier 的 Src1 format 诊断关联]]></title>
        <id>https://jinmoza.github.io/posts/llvm-amdgpu-mfma-src1-format-diagnostic/</id>
        <link href="https://jinmoza.github.io/posts/llvm-amdgpu-mfma-src1-format-diagnostic/"/>
        <updated>2026-11-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[修复 LLVM AMDGPU IR verifier 在报告 MFMA scale Src1 向量宽度错误时关联了错误 format 参数的问题。]]></summary>
        <content type="html"><![CDATA[<h2>摘要</h2>
<p>LLVM 的 AMDGPU IR verifier 在检查 MFMA scale intrinsic 时，能够正确判断 Src1/B 向量是否满足 B format 的宽度要求，但失败诊断附带了错误的参数：检查依据是参数 4 <code>BLGP</code>，打印的却是参数 5——A scale 的 <code>op_sel</code>。</p>
<p>本次修复把诊断上下文改为参数 4，并强化既有 verifier 回归测试，使参数 4 与参数 5 取不同常量，从而真正验证错误关联的是 B format。修改只影响非法 IR 的诊断对象，不改变 IR 的接受或拒绝结果，也不触及 intrinsic ABI、AMDGPU lowering、指令选择或机器码。</p>
<h2>上游交付</h2>
<ul>
<li>LLVM PR：<a href="https://github.com/llvm/llvm-project/pull/237737">llvm/llvm-project#237737 — AMDGPU Fix MFMA Src1 format diagnostic</a></li>
<li>完整补丁：<a href="https://github.com/llvm/llvm-project/pull/237737/files">PR #237737 Files changed</a></li>
<li>对应提交：<a href="https://github.com/jinmoza/llvm-project/commit/92564a6a1082963af94676f03e7761c87d51f8d1"><code>92564a6a1082</code></a></li>
</ul>
<h2>问题所在层级</h2>
<p>该检查位于 <code>llvm/lib/IR/VerifierAMDGPU.cpp</code>，属于 LLVMCore 中的 LLVM IR verifier，而不是 AMDGPU lowering、MachineVerifier 或 <code>llvm-mc</code>：</p>
<pre><code>LLVM IR Parser
  -&gt; LLVM IR Verifier
     -&gt; AMDGPU intrinsic 专用检查  &lt;- 问题位于这里
  -&gt; AMDGPULowerIntrinsics
  -&gt; SelectionDAG / GlobalISel
  -&gt; Machine IR / MC
</code></pre>
<p><code>llvm-as</code>、<code>opt</code> 和 <code>llc</code> 都会在正常流程中触发 IR 验证。非法调用会在进入 AMDGPU CodeGen 前被拒绝，因此这里的错误不会传播成错误指令；它的问题是把无关参数展示给开发者，误导对非法向量宽度的定位。</p>
<h2>参数语义与不一致</h2>
<p>MFMA scale intrinsic 中与本问题相关的参数是：</p>
<table>
<thead>
<tr>
<th>参数编号</th>
<th>含义</th>
<th>是否决定源向量宽度</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Src0 / A</td>
<td>被检查对象</td>
</tr>
<tr>
<td>1</td>
<td>Src1 / B</td>
<td>被检查对象</td>
</tr>
<tr>
<td>3</td>
<td><code>CBSZ</code>，A format</td>
<td>决定 A 所需宽度</td>
</tr>
<tr>
<td>4</td>
<td><code>BLGP</code>，B format</td>
<td>决定 B 所需宽度</td>
</tr>
<tr>
<td>5</td>
<td>A scale <code>op_sel</code></td>
<td>与 B 宽度无关</td>
</tr>
</tbody>
</table>
<p>Src0/A 的检查是自洽的：宽度由参数 3 <code>CBSZ</code> 决定，诊断也附带参数 3。Src1/B 的判定同样正确使用了参数 4 <code>BLGP</code>，但诊断上下文错误地取了参数 5：</p>
<pre><code>Check(Src1Ty-&gt;getNumElements() &gt;= GetFormatNumRegs(BLGP),
      "invalid vector type for format", &amp;Call, Src1,
      Call.getArgOperand(5));
</code></pre>
<p>因此这是一个明确的 operand index 错误，而不是 format 语义或宽度规则存在歧义。</p>
<h2>可区分的最小复现</h2>
<p>要证明诊断究竟引用了哪个参数，必须让参数 4 和参数 5 的值不同。例如让 B 为 <code>&lt;4 x i32&gt;</code>，同时设置：</p>
<pre><code>; 参数 4：BLGP = 0，B format 为 FP8，要求至少 8 个元素
; 参数 5：A scale op_sel = 2，与 B 宽度无关
call ...(&lt;8 x i32&gt; %a, &lt;4 x i32&gt; %b, ..., i32 0, i32 0, i32 2, ...)
</code></pre>
<p>此时 verifier 拒绝 IR 是正确的，因为 <code>4 &lt; 8</code>。修复前，诊断最后打印 <code>i32 2</code>，也就是无关的参数 5；正确结果应打印 <code>i32 0</code>，即实际决定 B 宽度的参数 4。</p>
<p>Src0/A 可以作为对照：让 A 过窄且参数 3 与参数 5 不同，诊断会正确打印参数 3。A、B 两条路径的对照进一步排除了诊断框架本身选错对象的可能性，问题就在 Src1 <code>Check</code> 调用传入的最后一个 operand。</p>
<h2>为什么既有测试没有发现</h2>
<p>既有 <code>llvm/test/Verifier/AMDGPU/mfma-scale.ll</code> 已覆盖 Src1/B 过窄，但参数 4 和参数 5 都是 <code>i32 0</code>。无论实现取哪个参数，FileCheck 看到的最后一行都是 <code>i32 0</code>，因此测试只能确认“打印了一个零”，不能确认这个零来自 <code>BLGP</code>。</p>
<p>回归测试的关键不是再增加一个同类非法调用，而是制造参数来源的可观察差异：保留参数 4 为 <code>i32 0</code>，把参数 5 改为 <code>i32 1</code>，并继续要求诊断打印 <code>i32 0</code>。这样，未来若再次把诊断关联到参数 5，测试会立即失败。</p>
<h2>修复与范围控制</h2>
<p>最终代码修复只有一处：</p>
<pre><code> Check(Src1Ty-&gt;getNumElements() &gt;= GetFormatNumRegs(BLGP),
       "invalid vector type for format", &amp;Call, Src1,
-      Call.getArgOperand(5));
+      Call.getArgOperand(4));
</code></pre>
<p>同时审计了 <code>verifyAMDGPUIntrinsicCall()</code> 中同级的 operand-aware 检查：MFMA Src0、format 范围、WMMA A/B、cooperative atomic 和 AV metadata 的判定对象与诊断对象一致，未发现另一个同类的“条件使用 X、诊断打印 Y”错误。部分检查只附带完整 call，精度较低，但不属于本次参数关联错误。</p>
<p>范围因此保持为：一行生产代码修复，加一处能够区分两个参数的测试调整。没有顺手重构诊断框架，也没有把它并入此前的 #236362 lowering 修复。</p>
<h2>验证结果</h2>
<p>验证先以测试改动建立 RED：在生产代码未修复时，测试期望参数 4 的 <code>i32 0</code>，实际却打印参数 5 的 <code>i32 1</code>。修改 operand index 后同一测试转为 GREEN。</p>
<p>最终补丁通过：</p>
<ul>
<li><code>llvm/test/Verifier/AMDGPU/mfma-scale.ll</code>：1/1；</li>
<li><code>llvm/test/Verifier/AMDGPU</code>：18/18；</li>
<li>完整 <code>llvm/test/Verifier</code>：444 PASS、10 UNSUPPORTED、0 FAIL；</li>
<li><code>git diff --check</code>；</li>
<li>独立 Codex review 与 code-simplifier 检查，未发现需要扩大的修改。</li>
</ul>
<p>这个修复的核心经验是：诊断测试不仅要检查输出值，还要让候选 operand 具有不同的可观察值。只有这样，测试才能证明诊断关联的是正确语义来源，而不是偶然打印了相同常量。</p>
]]></content>
        <author>
            <name>jinmoza</name>
            <uri>https://jinmoza.github.io/</uri>
        </author>
        <published>2026-11-01T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[你好，世界]]></title>
        <id>https://jinmoza.github.io/posts/hello-world/</id>
        <link href="https://jinmoza.github.io/posts/hello-world/"/>
        <updated>2026-10-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[这是博客的第一篇文章，也是一个新的开始。]]></summary>
        <content type="html"><![CDATA[<p>欢迎来到 <strong>Jin's Notes</strong>。</p>
<p>这是博客的第一篇文章，也是一个新的开始。以后我会在这里整理技术探索、项目实践和阶段性的思考，主题大致包括：</p>
<ul>
<li>系统性能与底层优化</li>
<li>编译器和异构计算</li>
<li>AI 工程与效率工具</li>
<li>值得长期积累的方法与经验</li>
</ul>
<p>网站使用 <a href="https://astro.build/">Astro</a> 构建，通过 GitHub Pages 自动发布。文章使用 Markdown 编写，因此内容始终以简单、可迁移的文件保存在自己的仓库中。</p>
<p>比起追求更新频率，我更希望这里留下经得起回看的内容。</p>
]]></content>
        <author>
            <name>jinmoza</name>
            <uri>https://jinmoza.github.io/</uri>
        </author>
        <published>2026-10-19T00:00:00.000Z</published>
    </entry>
</feed>