Skip to content

MLS 三个访存 Bug 深度剖析:从 descriptor 到数据通路的不变量

海光 DCU(类 AMD CDNA/GCN 架构,包括 gfx92a、gfx938、gfx946 等)在算子库中引入了硬件级矩阵加载机制(Matrix Load from Global to LDS,简称 MLS),如内建指令 __builtin_hcu_matrix_load_128X16_b8。该机制允许计算单元绕过通用 VGPR 中转,直接将显存中的二维分块异步搬运至共享内存(LDS),显著降低寄存器压力并提升数据搬运吞吐。

然而,在高性能算子从理论设计走向真实硬件的过程中,显存描述符(Buffer SRD)、虚拟地址翻译、硬件微架构与 IEEE 754 算术流水线交织,引发了一系列极其隐蔽且致命的访存缺陷。本文聚焦于 flash-attention-hygon 仓库中已被定位并修复的三个典型访存 Bug:

  1. Bug 1(commit abcba0c):SRD 基地址低 32 位累加引发跨 4 GiB 显存回绕;
  2. Bug 2(commit c5d9320 / 159bca8):行过滤掩码已全量屏蔽(nm_filter = 16)的尾块依然触发硬件 VM Fault;
  3. Bug 3(commit cbf89aa):双 Tile 预取边界错位读入脏显存 / 未初始化 LDS,引发 IEEE 754 0×NaN=NaN0 \times \text{NaN} = \text{NaN} 累加器毒化。

这不仅是一次缺陷复盘,更是为后续深入对比 NVIDIA Hopper 架构 TMA(Tensor Memory Access)与 cuTensorMap 奠定底层地基——通过彻底剖析 MLS 的程序员可见模型(Programmer-Visible Model),厘清硬件与软件在张量语义理解上的分界线。


方法论:三类事实的严格标注准则

Section titled “方法论:三类事实的严格标注准则”

为了杜绝”将微架构推测写成硬件事实”或”凭空捏造内部机制”的现象,本文对所有涉及硬件行为、指令语义及微架构特性的技术陈述,均严格归入以下三类,并在正文中显式标注:

  • 【代码/实验可确认】:能够从 commit diff、源码上下文、实际编译汇编(SASS/GCN ISA)或运行时复现验证的事实。
  • 【官方 ISA/文档可确认】:能够在 AMD GCN/CDNA 官方 ISA 手册、ROCm 文档、NVIDIA PTX/CUDA 官方文档中查证一手出处的事实(均附带具体文档章节或规范链接)。
  • 【推断的微架构行为】:厂商未公开微架构流水线细节,但根据硬件报错现象、外部黑盒测试以及计算机体系结构原理反推的合理行为假设。严禁将其断言为硬件真理。

第一部分:三大 Bug 的完整数据流模型

Section titled “第一部分:三大 Bug 的完整数据流模型”

在剖析各个缺陷前,首先必须建立海光 MLS 在 FlashAttention 中的端到端数据流动与状态流转模型。

在搭载 MLS 机制的 FlashAttention Kernel 中,一个张量分块(Tile)从全局显存到达矩阵乘累加器的物理路径如下:

Global Memory (HBM/DDR)
│
▼ [Software Address Generation & SRD Assembly]
Scalar General Purpose Registers (SGPR: vec4_uint)
│ [0:1] 64-bit Virtual Base Address
│ [2] Row Stride
│ [3] Control Bits (nm_filter, Interleave, Swizzle)
│
▼ [Issue MLS Instruction: matrix_load_128x16_b8]
MLS Hardware Engine / Memory Controller
│ 1. Address Generation Unit (AGU) evaluates {srsrc[1], srsrc[0]}
│ 2. TLB / MMU Virtual-to-Physical Address Translation
│ 3. Read Request Burst on Internal Memory Bus
│ 4. Data Return & Hardware Row-Masking
▼
Local Data Share (LDS)
│ Write directly to designated LDS byte offset (bypass VGPR)
│
▼ [DS Read: ds_read_matrix_format / ds_read_b128]
Vector General Purpose Registers (VGPR)
│
▼ [Matrix Multiply-Accumulate: __builtin_hcu_mmac_*]
MMA / Matrix Core Execution Units
│
▼
Accumulator Registers (VGPR: acc_s / acc_o)

针对上述数据流,各环节的职责与认知边界划分如下:

  1. 谁负责计算地址:
    • 软件负责部分【代码/实验可确认】:计算张量全局首地址指针、Batch/Head 偏移、Tile 行列索引、Warp 负责的行跨度(如 warp_id * WARP_M)、将其打包为 64 位规范虚拟地址填入 srsrc[0:1],并将行步长(row_stride)写入 srsrc[2]、过滤行数(nm_filter)与布局标志位写入 srsrc[3]。
    • 硬件负责部分【代码/实验可确认】:接收标量描述符后,硬件在内部根据基地址与 row_stride 展开生成针对该 Tile 的 2D 突发内存读请求,并将数据经由硬件直接总线跨层写入 LDS 的目标字节偏移处。
  2. 描述符 SRD(vec4_uint)字段结构【代码/实验可确认】:
    • 见 csrc/flash_attn_hipc/include/numeric_types.h:38 与 csrc/flash_attn_hipc/include/intrinsic_mls_ds.h:31-34:
      • srsrc[0]:全局显存基地址低 32 位(base_lo)。
      • srsrc[1]:全局显存基地址高 32 位(base_hi),有效位宽受制于虚拟地址上限。
      • srsrc[2]:二维矩阵的行步长(stride,单位通常为元素数或字节数)。
      • srsrc[3]:硬件控制位。其中 bits [15:8] 保存行过滤计数 (nm_filter << 8),bits [18:17] 为 0x40000(指定 4-interleave 存储交错模式),高位包含 Cache Swizzle 控制位。
  3. Warp 提供什么 Offset【代码/实验可确认】:
    • MLS 是一条Wavefront / Warp 级协同的标量化指令,其操作数 SRD 保存于标量寄存器(SGPR)中。整个 Warp 共享同一个 SRD 基地址。Warp 内部各 Lane 不向 MLS 硬件提供独立的散布(Scatter/Gather)偏移;Lane 内部的数据重排由后续从 LDS 读取到寄存器的 DS 指令(如 ds_read_matrix_format)配合 lane_id 完成。
  4. MLS 硬件知道哪些 Tensor 信息【代码/实验可确认】:
    • 静态分块几何形状(由指令操作码静态确定,例如 128x16 字节或 32x32 半精度元素);
    • 二维单步长(由 srsrc[2] 提供的 1D row stride);
    • 尾部行屏蔽深度(由 srsrc[3] 提供的 nm_filter);
    • 目标 LDS 写入基址与格式模式(Transpose / Interleave)。
  5. MLS 硬件不知道哪些 Tensor 信息【代码/实验可确认】:
    • 张量维度(Rank,是 2D、3D 还是 4D);
    • 张量逻辑几何形状(Batch Size、Num Heads、SeqLen、HeadDim);
    • 全局张量物理边界(无 extent、无张量总尺寸,无法判定当前行索引是否超出张量分配边界);
    • 高维步长(如 Batch Stride、Head Stride);
    • 越界安全语义(无越界自动截断、无越界自动补零,仅有粗粒度行过滤)。
  6. Filtering / Masking 发生在哪一个逻辑层:
    • 【代码/实验可确认】:软件仅能确认将 nm_filter 数值编码进 srsrc[3] 的控制位。
    • 【推断的微架构行为】:从观察到的硬件 Page Fault 现象反推,nm_filter 的生效层级必然晚于基地址的初始生成与 MMU 页表权限校验。AGU 发起访存事务时仍需解析基地址所在虚页;行过滤机制主要在突发请求下发、Cache 行丢弃或写入 LDS 的跨总线截断层生效。具体过滤发生在微架构哪一流水级,海光并未公开。

Bug 1:SRD 低 32 位累加引发 4 GiB 回绕

Section titled “Bug 1:SRD 低 32 位累加引发 4 GiB 回绕”

在前向 FP8 FlashAttention(csrc/flash_attn_hipc/include/fwd/gfx938/fp8_pv_gemm_utils_mls_ds.h:11-56)中,每个 Warp 需要预取 V 矩阵的两个 16 行 Tile 进入 LDS。 Kernel 试图根据 warp_id 计算该 Warp 所负责切片在显存中的物理基地址: Offset=(warp_id×2)×16×v_row_stride×sizeof(Element)\text{Offset} = (\text{warp\_id} \times 2) \times 16 \times \text{v\_row\_stride} \times \text{sizeof(Element)} 并试图将该偏移加到基地址 v_root 上,构造当前 Warp 专属的 v_srsrc 发起 MLS 传输。

  • 违反的不变量:
    1. 64-bit Pointer Arithmetic Invariant(64 位指针算术完整性不变量):显存虚拟指针是 64 位连续线性空间中的标量,任何物理偏移累加必须保证进位完整传播;
    2. Virtual-Address Validity Invariant(规范虚拟地址有效性不变量):下发至硬件描述符的地址必须严格落在张量分配的虚地址区间内。
  • 为什么是不变量且被隐藏: 在 C++ 类型系统中,vec4_uint 被定义为包含 4 个独立 uint32_t 元素的向量类型(numeric_types.h:38)。编译器将 v_srsrc[0] 视为纯粹的无符号 32 位整型。对 v_srsrc[0] 执行加法完全合法,编译器类型系统根本不知道 [0] 和 [1] 在底层硬件微架构中被拼接为一个 64 位的物理/虚拟基址。进位截断在语言层面静默发生,不产生任何编译警告。

在处理非 128 整数倍对齐的短序列或尾部 Tile 时(csrc/flash_attn_hipc/include/fwd/gfx938/fp8_qk_gemm_utils_mls_ds.h:20-65),部分 Warp 分配到的 16 行数据完全超出了当前序列的最大有效长度(max_seq_q_offset)。 Kernel 计算出 nm_filter = 16,即通知 MLS 硬件:“这一整块 16 行全部属于 Padding,请全部过滤掉,不要将其数据写入 LDS”。

  • 违反的不变量:
    1. Descriptor Validity Invariant(描述符输入有效性不变量):提交给硬件访存指令的描述符基指针,其指向的虚拟地址必须已建立合法的页表物理映射(Page Mapping);
    2. Tail-Tile Memory-Safety Invariant(尾部分块内存安全性不变量):即使分块在算法逻辑上被完全屏蔽,其物理寻址依然严禁跨入未映射页。
  • 为什么是不变量且被隐藏: 开发者受到”逻辑谓词(Logical Predication)等价于内存安全”的直觉误导。高级语言中的 if (valid) load() 会短路访存,但硬件描述符指令将过滤参数作为一个寄存器字段传入。程序员误以为硬件见到 nm_filter = 16 就会完全短路内部一切访存操作,忽视了硬件流水线在地址翻译阶段的硬性校验约束。

Bug 3:全过滤 Tile 脏 LDS 读入与 NaN 累加器毒化

Section titled “Bug 3:全过滤 Tile 脏 LDS 读入与 NaN 累加器毒化”

在 gfx92a 的 PagedAttention / FlashAttention FP8 实现(csrc/flash_attn_hipc/include/kvcache/gfx92a/fp8_qk_gemm_utils_mls_ds_gfx92a.h:72-102)中,每个 Warp 负责加载两个 16 行 Tile(Tile 1 与 Tile 2)。在序列尾端,Tile 2 完全超出序列边界。 Kernel 试图通过 Softmax 掩码将非法位置的注意力分数置为 −∞-\infty,使其对应的 Softmax 概率权重精确为 0.0f0.0f,期望在后续 GEMM 计算中通过乘以 00 自然抹平这部分未对齐数据。

  • 违反的不变量:
    1. Numerical-Safety Invariant(数值安全性不变量):进入浮点计算流水线的所有操作数,其二进制位模式必须对应合法的非 NaN 浮点数(或在进入 FMA 前完成物理置零);
    2. Invalid-Element Sanitization Invariant(无效元素数据净化不变量):不能依赖算法层面的置零(Semantic Masking)替代物理存储层面的净化(Data Sanitization)。
  • 为什么是不变量且被隐藏: 初等数学代数恒等式 0×x=00 \times x = 0 深入人心,但 IEEE 754 浮点标准规定 0.0×NaN=NaN0.0 \times \text{NaN} = \text{NaN},且 0.0×±∞=NaN0.0 \times \pm\infty = \text{NaN}。类型系统将 FP8 / FP16 视为普通数据位,无法静态检测内存未初始化时的脏位模式是否恰好命中了 NaN。累加器一旦被 NaN 污染,后续所有乘累加操作全盘报废。

第二部分:三大 Bug 的底层技术深挖

Section titled “第二部分:三大 Bug 的底层技术深挖”

Bug 1 深挖:SRD 低 32 位累加与跨 4 GiB 回绕

Section titled “Bug 1 深挖:SRD 低 32 位累加与跨 4 GiB 回绕”

1. 64 位虚拟地址在描述符中的拆解与进位丢失推导

Section titled “1. 64 位虚拟地址在描述符中的拆解与进位丢失推导”

在海光 DCU / AMD GCN 硬件上,MLS 指令要求的 128 位缓冲资源描述符(SRD)使用两个连续的 32 位寄存器表示基地址: BaseAddress[63:0]=(uint64_t(srsrc[1])≪32)∣uint64_t(srsrc[0])\text{BaseAddress}[63:0] = (\text{uint64\_t}(\text{srsrc}[1]) \ll 32) \mid \text{uint64\_t}(\text{srsrc}[0])

原缺陷代码(commit abcba0c 修复前):

// 缺陷代码:csrc/flash_attn_hipc/include/fwd/gfx938/fp8_pv_gemm_utils_mls_ds.h:28
v_srsrc[0] = (nm_filter == 16) ? v_root[0] : v_root[0] + (warp_id * 2) * 16 * v_row_stride * sizeof(Element);

其底层汇编展开与寄存器数据流如下:

64-bit Virtual Address: v_root
┌──────────────────────┬──────────────────────┐
│ v_root[1] (High 32) │ v_root[0] (Low 32) │
└──────────────────────┴──────────────────────┘
│
▼ [32-bit Add: + Offset]
┌──────────────┐
│ Carry Bit: C │ ──┐ Carry C is DISCARDED!
└──────────────┘ │ (No s_addc to v_srsrc[1])
│ ▼
┌──────────────────────┬──────────────────────┐
│ v_srsrc[1] (UNCHANGED│ v_srsrc[0] (WRAPPED) │
└──────────────────────┴──────────────────────┘

当 v_root[0] + Offset >= 2^32 时,硬件 ALU 的进位标志位 CC 被无情丢弃,v_srsrc[1] 保持原值不变。

通过一组极端但真实的数值模拟该场景:

  • 假设显存分配器分配的张量基地址跨越了 4 GiB 边界前夕: v_root = 0x0000_0001_FFFF_F000(即十进制约 8.58 GB 处)。 拆解入描述符:
    • v_root[1] = 0x0000_0001
    • v_root[0] = 0xFFFF_F000
  • 假设当前 Warp 计算出的 Tile 偏移量仅为一个微小的值: Offset = 0x0000_2000(即十进制 8192 字节,仅 8 KiB)。
  • 数学上正确的 64 位虚拟地址: Correct Address=0x1_FFFF_F000+0x2000=0x0000_0002_0000_1000\text{Correct Address} = \text{0x1\_FFFF\_F000} + \text{0x2000} = \text{0x0000\_0002\_0000\_1000} 在此地址下,srsrc[1] 应进位变为 0x0000_0002,srsrc[0] 为 0x0000_1000。
  • 原缺陷代码计算出的实际地址: v_srsrc[0] = 0xFFFF_F000 + 0x2000 = 0x0000_1000(32 位无符号整数溢出回绕); v_srsrc[1] 未被触碰,仍保持 0x0000_0001。 拼接后的硬件访问地址为: Actual Address=0x0000_0001_0000_1000\text{Actual Address} = \text{0x0000\_0001\_0000\_1000}
  • 后果: Δ=Correct−Actual=0x0000_0001_0000_0000=232=4 GiB\Delta = \text{Correct} - \text{Actual} = \text{0x0000\_0001\_0000\_0000} = 2^{32} = 4\text{ GiB} 硬件 MLS 指令直接从比预期整整低 4 GiB 的显存地址拉取数据!若该地址未映射,立即引发致命的 GPU VM Fault 崩溃;若该地址恰好属于其他张量或 PyTorch 显存池,算子将静默读入野数据,导致计算精度彻底损毁且极难定位。

3. 为什么”我只是加一个很小的 Offset”也不能默认只更新低 32 位

Section titled “3. 为什么”我只是加一个很小的 Offset”也不能默认只更新低 32 位”

GPU 程序员常有经验主义偏见:“我的 Tile 偏移最多几兆字节,32 位整数上限有 4 GB,怎么可能溢出?” 这一直觉混淆了相对偏移量(Relative Offset)的量级与基地址在地址空间中的绝对位置(Absolute Position)。 显存分配器在返回虚拟指针时,并不保证低 32 位从 0 开始。只要基地址的低 32 位碰巧接近 0xFFFF_FFFF(例如分配在 4GB - 64KB 处),哪怕只是加 1 个字节的偏移,都会跨越 4 GiB 物理边界并产生跨字进位。因此,任何在 32 位域对基地址截断更新的行为都是灾难性的。

通用原则:当软件负责操作底层硬件描述符(Descriptor Manipulation)时,硬件暴露给开发者的地址表示越底层、越碎片化(如将 64 位指针拆为独立 32 位寄存器),软件违背隐藏不变量的概率就越高。硬件应当要么接受完整的 64 位原生指针,要么提供带自动进位语义的高级接口。

5. 补遗:VA_LIMIT_BITS(x) 48 位掩码的微架构与 ISA 根源

Section titled “5. 补遗:VA_LIMIT_BITS(x) 48 位掩码的微架构与 ISA 根源”

修复代码中引入了宏 VA_LIMIT_BITS:

// csrc/flash_attn_hipc/include/intrinsic_mls_ds.h:8
#define VA_LIMIT_BITS(x) (0xffffffffffff & x)

掩码 0xffffffffffff 对应二进制精确的低 48 位。

  • 【官方 ISA/文档可确认】: 根据 AMD GCN/CDNA 架构规范(AMD “Vega” 7nm / CDNA Instruction Set Architecture,第 8 章 Memory Basics),GPU 的内存管理单元(IOMMU / GPU VM)遵循与现代 x86-64 架构一致的 48 位规范虚拟地址(Canonical 48-bit Virtual Address) 空间,支持最大 256 TiB 寻址。
  • 【代码/实验可确认】: 在海光 DCU 的 Buffer Resource Descriptor 规范中,描述符的 BASE_ADDRESS 字段实际占用 48 位:Word 0 容纳低 32 位,Word 1 的 bits [15:0] 容纳高 16 位。Word 1 的高 16 位(bits [31:16])被保留用于格式标志或必须清零。 若 64 位指针加法产生符号扩展或高位溢出杂散位(如第 48 至 63 位非零),直接写入 srsrc[0:1] 会污染 Word 1 的控制字段,引发非法描述符异常。应用 VA_LIMIT_BITS 确保了虚拟地址高 16 位被物理擦除,符合硬件规范。

Bug 2 深挖:完全过滤的尾块依然触发 VM Fault

Section titled “Bug 2 深挖:完全过滤的尾块依然触发 VM Fault”

1. 核心灵魂:四层概念的严格解耦辨析

Section titled “1. 核心灵魂:四层概念的严格解耦辨析”

理解 Bug 2 的本质,必须在认知层面清晰解耦以下四个概念:

┌─────────────────────────────────────────────────────────────┐
│ 1. Logical Predicate │
│ 算法/程序逻辑判定:当前元素属于 Padding,不参与计算 │
└──────────────────────────────┬──────────────────────────────┘
│ (Does NOT imply)
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Address Validity │
│ 指针数学有效性:Base + Offset 计算出的地址是否在张量合法范围│
└──────────────────────────────┬──────────────────────────────┘
│ (Does NOT imply)
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Page Mapping │
│ 操作系统/IOMMU 状态:该虚拟地址在 GPU 页表中有无物理页映射 │
└──────────────────────────────┬──────────────────────────────┘
│ (Does NOT imply)
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Memory Transaction │
│ 硬件微架构行为:总线与 AGU 是否实际发出读请求与地址翻译 │
└─────────────────────────────────────────────────────────────┘
  • Logical Predicate(逻辑谓词):nm_filter == 16。业务逻辑明确知道该 Tile 毫无意义。
  • Address Validity(地址合法性):ptr = q_root + out_of_bounds_offset。算出的虚拟指针已经跨过了张量真实的 shape 范围。
  • Page Mapping(页映射状态):若张量紧凑分配且尾端恰好处于内存页(如 4 KiB 或 2 MiB 大页)的边界,越界地址落入未向操作系统申请的空洞虚存区,页表中不存在有效 PTE(Page Table Entry)。
  • Memory Transaction(硬件访存事务):硬件在执行 MLS 指令时,是否会因 nm_filter == 16 而在 AGU 级直接放弃地址翻译?

结论:Logical Tile Invalid + nm_filter = 16 绝不等价于 Physical Memory Access is Safe!

  • 【代码/实验可确认】:实验证明,即使向 srsrc[3] 传入了全屏蔽的 nm_filter = 16,当 srsrc[0:1] 为未映射的越界地址时,硬件依然坚决抛出 VM Fault(Illegal Memory Access)导致整卡掉卡。
  • 【推断的微架构行为】:海光 MLS 硬件流水线在设计上,将基地址的合法性校验(AGU / TLB Lookup)置于数据过滤流水线之前。硬件不会因为控制字里的掩码全为 1,就短路掉对指令基地址的操作数有效性检查。

2. 修复方案的本质:Safe-Address Remapping

Section titled “2. 修复方案的本质:Safe-Address Remapping”

提交 c5d9320、159bca8 与 cbf89aa 最终放弃了”依赖硬件 filter 保证安全”的幻想,采用了显式的安全地址重映射模式(Safe-Address Remapping):

// 源码对照:csrc/flash_attn_hipc/include/fwd/gfx938/fp8_qk_gemm_utils_mls_ds.h:34-36
const bool q_need_filter = max_seq_q_offset > 0 && max_seq_q_offset < 128;
int nm_filter = inline_min_max<0, 16>(32 * warp_id + 16 - max_seq_q_offset);
// 核心修复:当且仅当全过滤时,强制将基址回退至已知的合法首地址(偏移归 0)
*(uint64_t*)&q_srsrc = VA_LIMIT_BITS(*(uint64_t*)&q_root + ((q_need_filter && nm_filter == 16)
? 0
: int64_t(warp_id) * WARP_M * int64_t(q_row_stride) * sizeof(Element)));
q_srsrc[3] = 0x40000 + (q_need_filter ? (nm_filter << 8) : 0);

其工作机制可用通用 Kernel 模式归纳为: safe_ptr=is_fully_invalid  ?  known_good_ptr:real_ptr\text{safe\_ptr} = \text{is\_fully\_invalid} \;?\; \text{known\_good\_ptr} : \text{real\_ptr}

双保险机制协同工作:

  1. AGU 视角:基地址被强行拉回 q_root + 0,该地址位于张量起始区域,必然拥有合法的物理页映射(Page Mapping 必定合法),MMU 地址翻译通过,零 VM Fault;
  2. LDS 视角:srsrc[3] 中配置的 nm_filter = 16 发挥作用,通知硬件在数据返回阶段过滤该行,避免向 LDS 写入无效覆盖。

3. 主流生态中的同类 Workaround 与架构设计调研

Section titled “3. 主流生态中的同类 Workaround 与架构设计调研”

其他主流 GPU 生态是如何解决尾块越界访存的?是靠硬件保证越界安全,还是靠软件重映射?

生态 / 框架核心机制处理策略(硬件保证 vs 软件规避)具体实现出处与规范说明
AMD ROCm / Composable Kernel (CK)buffer_load_* 与 Descriptor Bounds硬件原生保证安全(限制于标准 Buffer 描述符)【官方 ISA/文档可确认】:在 AMD 标准 GCN/CDNA ISA 中,128 位 Buffer SRD 的 Word 2 包含 NUM_RECORDS 字段。硬件在执行 buffer_load 时会自动校验 offset < NUM_RECORDS,越界硬件直接静默返回 0,绝不产生 VM Fault。但在 flat 寻址或自定义 MLS 指令中由于缺乏该字段,退化为需软件保证。出处:AMD CDNA ISA Manual - Buffer Instructions。
Mesa AMDGPU (RADV / ACO)robustBufferAccess驱动/编译器两级兜底【官方文档可确认】:Vulkan 规范强制要求的 robustBufferAccess 特性。在 ACO 编译器中,若使用 Buffer 描述符,直接借用硬件 NUM_RECORDS 边界检查;若使用 64 位 Flat 指令,编译器前端会发射 v_cndmask_b32 将越界偏移强制重映射为合法偏移 0。出处:Mesa RADV Driver Robustness Implementation。
CUDA / PTXcp.async 谓词化与 ignore-src硬件指令级规避【官方 ISA/文档可确认】:PTX ISA 7.5+ 引入了带有 src-size 与 ignore-src 修饰符的异步拷贝指令:cp.async.ca.shared.global [dst], [src], cp-size, ignore-src;。当传入 ignore-src 时,硬件完全忽略源地址 [src],不发射任何全局内存事务,且自动向 [dst] 填零。出处:NVIDIA PTX ISA 8.1 - cp.async。
CUTLASSPredicatedTileIterator软件谓词驱动 + 哑地址填充【代码可确认】:CUTLASS 2.x 的 predicated_tile_iterator.h 中,通过为每个线程维护布尔谓词掩码(guard)。在发射 load() 时,对 guard == false 的通道跳过加载或重置指针,确保未映射地址不被传入指令。出处:cutlass/transform/threadblock/predicated_tile_iterator.h。
Tritontl.load(mask, other)编译器前端谓词下推【官方文档可确认】:Triton 语言语义 tl.load(ptr, mask=mask, other=0.0) 在 Lowering 到 LLVM/PTX 时,将 mask 编译为 PTX 谓词指令 @%p ld.global。被 Mask 掉的线程在指令发射阶段即被硬件执行单元屏蔽,不执行地址翻译。出处:Triton Documentation - tl.load。

调查总结: 除标准 AMD Buffer 指令利用 NUM_RECORDS 由硬件处理越界外,几乎所有先进体系(NVIDIA PTX、Triton、CUTLASS)均遵循”绝不让非法虚拟地址进入硬件 MMU”的设计准则。当硬件指令集缺乏细粒度的单指令源地址抑制能力时,软件安全地址重映射(Safe-Address Remapping)是跨平台共识的通用工程准则。


Bug 3 深挖:脏 LDS、NaN 传播与累加器毒化

Section titled “Bug 3 深挖:脏 LDS、NaN 传播与累加器毒化”

1. 为什么”后面 Mask 掉”根本救不回来?

Section titled “1. 为什么”后面 Mask 掉”根本救不回来?”

在 FlashAttention 的经典算法逻辑中: S=QKT,P=Softmax(S+Mask),O=PVS = Q K^T, \quad P = \text{Softmax}(S + \text{Mask}), \quad O = P V 对于尾部 Padding 的 Token,因果掩码或长度掩码会将其 Attention Logits 赋为 −∞-\infty: Sij=−∞  ⟹  Pij=e−∞∑eS=0.0fS_{ij} = -\infty \implies P_{ij} = \frac{e^{-\infty}}{\sum e^S} = 0.0f 开发者潜意识认为:既然权重矩阵 PijP_{ij} 已经精确为 0.0f0.0f,那么 Vj,:V_{j,:} 哪怕读入任何垃圾数值,0×V0 \times V 都会变成 00。

2. IEEE 754 严格数学推导与累加器毒化(Accumulator Poisoning)

Section titled “2. IEEE 754 严格数学推导与累加器毒化(Accumulator Poisoning)”

根据 IEEE 754 浮点数算术标准(IEEE Standard for Floating-Point Arithmetic):

  • 规则 1:0.0×NaN=NaN0.0 \times \text{NaN} = \text{NaN}(无效算术操作异常,Invalid Operation Exception);
  • 规则 2:0.0×(+∞)=NaN0.0 \times (+\infty) = \text{NaN},以及 0.0×(−∞)=NaN0.0 \times (-\infty) = \text{NaN};
  • 规则 3:NaN+x=NaN\text{NaN} + x = \text{NaN}(NaN 具有绝对的代数传染性)。

当 gfx92a 的 MLS 尾块读取因逻辑错误越界或者 Tile 全过滤导致 LDS 写入被硬件跳过时:

  1. LDS 的脏数据残留【代码/实验可确认】:LDS 作为 GPU 块内共享暂存器,在 Block 分配时不会进行任何硬件级自动清零。其内部充满上一轮迭代、上一个 Kernel 遗留的随机垃圾数据位;
  2. 位模式命中 NaN【代码/实验可确认】:在 FP8 格式(如 E4M3 或 E5M2)下,指数位全为 1 的位模式极为紧凑,随机脏内存极高概率落在 NaN(或 Inf)的表示区间内;
  3. MMA / FMA 流水线坍塌: acc_o=acc_o+(Pij×Vjk)=acc_o+(0.0f×NaN)=acc_o+NaN=NaN\text{acc\_o} = \text{acc\_o} + (P_{ij} \times V_{jk}) = \text{acc\_o} + (0.0f \times \text{NaN}) = \text{acc\_o} + \text{NaN} = \text{NaN}
  4. 累加器毒化(Accumulator Poisoning):一旦累加器寄存器 acc_o 的某一位变为 NaN,后续所有循环迭代中无论乘上多么合法的 PP 与 VV,NaN+Normal≡NaN\text{NaN} + \text{Normal} \equiv \text{NaN}。最终 Attention 的整行输出彻底报废。
  • Semantic Masking(语义掩码):在算法和数学逻辑层面,通过将注意力权重置零来表达”不关注该位置”;
  • Data Sanitization(数据物理净化):在物理比特层面,确保流入硬件计算单元(MMA/FMA)的每一字节数据都是合法的、有限的浮点数表示。

核心原则: Invalid tensor elements must be sanitized before they enter the compute datapath. (无效张量元素在踏入硬件计算流水线之前,必须在物理存储层面完成净化与清零。)

4. NVIDIA 跨代际同类问题演进与硬件级消解

Section titled “4. NVIDIA 跨代际同类问题演进与硬件级消解”

NVIDIA GPU 架构在演进过程中,同样经历了从”软件防御”到”硬件消除”的技术路线:

Volta / Turing (SM70 / SM75)
└─ Software Predication: @%p ld.global + 显式寄存器清零 (v_mov_b32)
│
Ampere (SM80)
└─ cp.async 硬件 Zero-Fill + ignore-src 修饰符 (硬件自动填零共享内存)
│
Hopper (SM90)
└─ TMA (Tensor Memory Access) 边界感知的 OOB Handling
├─ CU_TENSOR_MAP_FLOAT_OOB_FILL_NAN_REQUEST_ZERO_FMA
└─ SASS 指令级断路器: HFMA2.OOB (检测 OOB-NaN 并强行箝位为 +0.0)
  • SM70/75(Volta/Turing):完全依赖软件。编译器发射谓词控制加载,在 shared memory 或寄存器中显式执行 mov 填零。
  • SM80(Ampere):cp.async 在硬件层面引入 src-size。当拷贝长度小于分块长度时,硬件自动将共享内存超出部分补零,切断脏内存进入计算通路的可能。
  • SM90(Hopper)TMA 与 HFMA2.OOB【官方 ISA 与实验验证】: 在 NVIDIA Hopper 架构中,张量描述符(CUtensorMap)直接在驱动层配置越界填充模式: CU_TENSOR_MAP_FLOAT_OOB_FILL_NAN_REQUEST_ZERO_FMA。 当 TMA 搬运越界时,硬件向共享内存写入一种特殊的 OOB-NaN 编码。 而在计算端,PTX ISA 8.1 引入了 fma.oob 指令修饰符。通过在集群机器(CUDA 12.8,-arch=sm_90)上反汇编实际验证,编译器发射出底层的专用 SASS 指令:
    HFMA2.OOB R11, R2, R5, R6 ;
    微架构行为:当 HFMA2.OOB 指令的输入操作数检测到 OOB-NaN 时,计算单元在硬件电路上强行将该乘法项的积截断置为 +0.0f,从而在算术执行级彻底消除了 0×NaN=NaN0 \times \text{NaN} = \text{NaN} 的累加器毒化风险!

第三部分:MLS 到底是什么抽象?

Section titled “第三部分:MLS 到底是什么抽象?”

为后续与 NVIDIA Hopper TMA / cuTensorMap 进行严密对照打好地基,在此必须对海光 MLS 的抽象本质进行严格的技术界定。

程序员可见模型(Programmer-Visible Model)

Section titled “程序员可见模型(Programmer-Visible Model)”

在开发者视角中,MLS 呈现为如下调用界面:

┌─────────────────────────────────────────────────────────────┐
│ 1. 软件构造 128 位标量描述符 SRD (vec4_uint): │
│ srsrc[0:1] = 64-bit Virtual Base Address (Canonical 48b) │
│ srsrc[2] = 1D Row Stride │
│ srsrc[3] = { nm_filter[15:8], interleave_mode[18:17] } │
├─────────────────────────────────────────────────────────────┤
│ 2. 软件指定写入目标: │
│ LDS Byte Address (soffset) │
├─────────────────────────────────────────────────────────────┤
│ 3. 发射硬件矩阵加载指令: │
│ matrix_load_128x16_b8 srsrc, lds_addr, moffset lds │
└─────────────────────────────────────────────────────────────┘

针对张量核心属性,逐项判定 MLS 硬件是否具有认知能力:

  1. Tensor Rank(张量阶数/维度数):
    • 判定:Unknown(硬件完全不知晓)【代码/实验可确认】。
    • 依据:描述符中没有任何表示维度的字段。无论是 2D 矩阵还是 4D 注意力张量,软件都必须将其扁平化为单一基地址。
  2. Logical Dimensions(逻辑尺寸:B, H, S, D):
    • 判定:Unknown(硬件完全不知晓)【代码/实验可确认】。
    • 依据:硬件不知道序列长度或 Head 维度,边界必须由软件换算为行数过滤。
  3. Tensor Bounds(张量全局物理边界):
    • 判定:Unknown(硬件完全不知晓)【代码/实验可确认】。
    • 依据:与 AMD 标准 Buffer 描述符(含 NUM_RECORDS)不同,MLS 的描述符字段被改造用于存放步长与掩码,不包含总长度边界字段。
  4. Multidimensional Coordinates(多维张量坐标):
    • 判定:Unknown(硬件完全不知晓)【代码/实验可确认】。
    • 依据:开发者无法像 TMA 一样向指令传入 (coord_x, coord_y, coord_z),必须在寄存器中手动算妥 64 位字节虚地址。
  5. Strides(步长体系):
    • 判定:Partial(仅知晓 1D 行步长)【代码/实验可确认】。
    • 依据:srsrc[2] 仅支持单一维度的行跨度,无法处理多维步长跳转。
  6. Tile Shape(分块几何尺寸):
    • 判定:Static / Hardcoded(指令级静态固化)【官方 ISA/代码可确认】。
    • 依据:分块尺寸硬编码在指令助记符与 Opcode 中(如 128x16_b8、32x32_b16),不可在描述符中动态配置。
  7. Swizzle / Interleave(存储交错与重排):
    • 判定:Hardware Assisted(硬件位控制)【代码/实验可确认】。
    • 依据:通过 srsrc[3] 的控制位和指令后缀(lds_trans / lds_rearrange),硬件支持在数据写入 LDS 时完成转置或 4-interleave 打散。
  8. OOB Semantics(越界语义与保护):
    • 判定:Coarse Row Masking Only(仅粗粒度行过滤,无保护安全区)【代码/实验可确认】。
    • 依据:仅提供基于行数的 nm_filter;不执行虚址保护短路,不对越界填充 0。

MLS 更接近于哪一种抽象? 定论:MLS 明确属于 B 类——它是一个高吞吐的 Matrix-Shaped 向量访存指令,但张量的绝大部分结构化语义依然完全依赖软件编码在裸地址与过滤字中。

判定依据与架构评价: MLS 绝非 NVIDIA Hopper TMA 那种真正意义上的多维张量异步 DMA 引擎。它的本质是 AMD CDNA 体系下针对 GEMM 访存拓扑深度定制的块状加载指令(Block Load to LDS with Stride & Packing)。它将张量降维、高维坐标折叠、物理页边界检查与未对齐保护的重担全盘抛给了算子工程师,这也正是前述三大严重访存 Bug 得以频繁滋生的架构级根源。


第四部分:三大 Bug 的共性归纳与跨体系映射

Section titled “第四部分:三大 Bug 的共性归纳与跨体系映射”

我们将上述三个缺陷归纳到统一的技术框架中,并给出通往 NVIDIA TMA 体系的对照映射:

维度Bug 1:SRD 4GB 回绕Bug 2:尾块 VM FaultBug 3:脏 LDS NaN 毒化
违反的不变量64-bit Pointer Arithmetic Invariant
Virtual-Address Validity Invariant
Descriptor Validity Invariant
Tail-Tile Memory-Safety Invariant
Numerical-Safety Invariant
Invalid-Element Sanitization Invariant
失效发生的层级软件地址生成层
(未向高 32 位传播进位)
硬件地址翻译层
(AGU / TLB 触碰未映射虚页)
计算数据通路层
(MMA / FMA 流水线执行 0×NaN0 \times \text{NaN})
为什么类型系统不报警vec4_uint 向量将 64 位地址撕裂为独立 uint32_t,语言层面加法完全合法描述符类型无法静态约束指针背后的物理页表分配状态浮点类型系统仅关注位数,无法静态约束位模式是否命中 NaN
运行时症状显存寻址越界 4 GiB,报 Illegal Access 或严重静默数据损毁尾部 Block 必定偶发触发 GPU Page Fault 致命崩溃累加器被不可逆毒化,Attention 输出大面积雪崩式 NaN
修复策略将前 64 位提升为原生指针运算,加法后配合 VA_LIMIT_BITS 掩码安全地址重映射(Safe-Address Remapping),全过滤时回退至首地址精确校准 Tile 偏移与过滤掩码,必要时保留合法行加载物理冲刷脏内存
通往下一篇:若硬件知晓张量语义,该 Bug 还存在吗?彻底消除。
TMA 接受 64 位 Tensor Base,软件只传坐标 (x,y),无手工拆解描述符寄存器。
彻底消除。
TMA 硬件感知 tensorDim,自动在张量外边界裁断请求,绝不向 MMU 发射非法事务。
彻底消除。
TMA 硬件支持 OOB_FILL_ZERO 或配置 OOB-NaN 结合 HFMA2.OOB,硬件电路上强行把积置零。

海光 MLS 的三个经典访存缺陷,展示了现代 GPU 在向专用张量加速演进过程中,底层指令集抽象与高层张量代数语义之间的脱节:

  1. 当硬件要求软件手动拼接描述符时,算术进位的丢失只是时间问题;
  2. 当硬件将掩码当作数据过滤而非事务短路时,页表不变量必然被击穿;
  3. 当硬件不对无效区域提供物理置零兜底时,IEEE 754 的代数异常必然污染整个矩阵核心。

这些缺陷的出现与修复,绝非个别工程师的粗心,而是计算体系结构在特定代际下的必然阵痛。在下一篇研究中,我们将把视野投向完全知晓多维张量语义的 NVIDIA Hopper TMA 架构与 cuTensorMap 机制,深入剖析其硬件坐标生成单元、异步屏障(mbarrier)、以及多维 Tensor Layout 在硬件层级是如何被彻底消解与保护的。