tune_gemm 设计
设计目标
tune_gemm 是 tune 的参考领域:一个分块参数很重要、正确性可以精确检查、且每个候选都可以序列化的问题。它在矩阵乘法上展示了完整的调优规范(先验证再计时、统计工作区、测试尾部、记录配置和环境),而不依赖于特定硬件的内核。
数学背景
乘积及其布局
对每个批次 b,Cb=AbBb,其中 cij=∑l=0k−1ailblj,代价为 2mnk 次浮点运算。因此每次操作 t µs 的测量时间对应于
GFLOP/s=t⋅1032mnk⋅batch.
一个 r×c 矩阵把第 b 个批次的元素 (i,j) 存放在 brc+ic+j(行主序布局)或 brc+jr+i(列主序布局);这两个映射都是从索引三元组到 [0,batch⋅rc) 的双射,因此每种布局组合描述的都是同一个数学乘积。
分块与工作区
gemm_blocked 把迭代空间划分为 mc×nc×kc 块,再把每个块划分为 mr×nr 寄存器分片。一个块涉及 A 的一个 mc×kc 块、B 的一个 kc×nc 面板和 C 的一个 mc×nc 块:
W=8(mc⋅kc+kc⋅nc+mc⋅nc) bytes,I=W2mcnckc=4(mc⋅kc+kc⋅nc+mc⋅nc)mcnckc flop/byte.
对于立方体 mc=nc=kc=s,W=24s2 且 I=s/12:块越大,每个加载的值被复用得越频繁,直到 W 超出本应容纳它的缓存。调优搜索的正是这种权衡,而 workspace_bytes 就是 W,被 valid_candidate 用作硬性上限。默认候选中最大的 128×128×64 需要 W=8(8192+8192+16384)=262144 字节。11 K. Goto 和 R. A. van de Geijn,“Anatomy of high-performance matrix multiplication”,ACM TOMS 34(3),2008。
分块内核的精确性
对每个元素,gemm_reference 计算
c^ij=fl(⋯fl(fl(0+ai0b0j)+ai1b1j)⋯+ai,k−1bk−1,j).
gemm_blocked 在第一个 k 块中从 0.0 开始计算每个元素,把部分和存入 C(双精度数,因此存储是精确的),在下一个 k 块中重新加载并按 l 递增的顺序继续。它以相同的顺序对相同的操作数执行相同的运算,因此只要编译器对两个循环一视同仁(特别是不把其中之一收缩为融合乘加),两个结果就逐位相同。打包只是复制值。因此对内置内核,正确性检查可以使用容差 0。
对求和重新排序的内核(向量化的微内核、不同的循环顺序)只能做到接近。设单位舍入误差 u=2−53、γk=ku/(1−ku),任何求和顺序都满足22 N. J. Higham,Accuracy and Stability of Numerical Algorithms,第 2 版,SIAM,2002,§3.1。
∣c^ij−cij∣≤γkl∑∣ail∣∣blj∣,
因此两个这样的内核至多相差 2γk∑l∣ail∣∣blj∣。对于来自 reproducible_matrix 的元素,∣a∣,∣b∣≤1,界为 2γkk≈2k2u:当 k=64 时约为 9.1⋅10−13。这就是在这些输入上为重排序内核传给 validate_gemm 的绝对容差。
reproducible_matrix(seed, r, c, layout) 运行 64 位线性同余生成器
xj+1=6364136223846793005xj+1442695040888963407(mod264),x0=seed⊕0x9E3779B97F4A7C15,
并把每个状态映射为 v=((x≫32)mod2001)/1000−1。乘数满足 ≡1(mod4) 且增量为奇数,因此由 Hull–Dobell 定理,该生成器具有满周期 264。32 位值 x≫32 对 2001 取模:由于 232=2146410⋅2001+886,2001 个值中有 886 个出现的可能性略高,相对高出 1/2146410≈4.7⋅10−7。元素是 [−1,1] 中 0.001 的倍数。逻辑矩阵按行主序生成,然后以请求的布局存储,因此布局不会改变值。
尾部
边缘分片处理不是块大小倍数的 m、n 或 k。boundary_shapes 返回 m−1、m 和 m+1:当 m 是 mr 的倍数时,它们分别考验短分片、恰好整除和单行尾部。
设计决策
可移植的标量内核
问题。 真实的 GEMM 调优依赖于因目标而异的 SIMD 微内核。选择。 一个标量分块内核,以 microkernel_id 作为标签。理由。 该包在每个后端上演示并测试调优机制;测试框架可以把该标签映射到自己的内核。
先验证再测量
gemm_is_correct 在候选被计时之前就运行它并与参考比较。对于无效的候选或形状,execute_candidate 返回 None,而不是返回一个看起来像快速空乘法的结果。
工作区作为硬约束
候选所需的内存由其参数算出,并在执行前与上限比较,因此搜索永远不会选中一个无法在目标内存预算内运行的配置。
显式的计时范围
TimingScope 和 AllocationMode 是候选及其 id 的一部分,因此“仅计算”和“端到端”的结果永远不会被当作测量同一事物来比较。
序列化的配置
config_json 写出重复一次调优运行所需的一切,64 位值写为字符串,模式版本为 mmkts_1。它与 GemmEnvironment 一起说明调优结果在何处有效。
正确性与不变量
- 在上述条件下,
gemm_blocked 与 gemm_reference 逐位相等;测试检查了带尾部的全部八种布局组合。
- 对每个有效候选都有
workspace_bytes(c) <= limit。
enumerate_gemm_candidates 以相同的顺序返回相同的 486 个候选,它们在上限 262144 下全部有效。
reproducible_matrix 只取决于其参数;行主序和列主序的结果描述同一个逻辑矩阵。
validate_gemm 把长度差异中的每个元素都计为不匹配,因此被截断的结果永远不会有效。
被否决的方案
- 针对特定目标的微内核。 无法在 MoonBit 后端之间移植。
- 验证中使用相对容差。 C 的元素可能接近零;对有界输入而言,上面的绝对界才是诚实的界。
- 用平台随机生成器生成输入。 无法跨目标复现。
边界
- 只有
m_n_k 循环顺序和标量代码;三个微内核 id 运行同一个循环。
pack_operand 把整个操作数复制为行主序;它不构建缓存大小的打包面板。
allocation_mode、timing_scope 和 reuse_count 是描述性的:内核在每次调用时都分配其结果;计时什么由测试框架决定。
- 没有搜索循环:由
tune 和应用驱动搜索。
reproducible_matrix 生成一个矩阵;批量操作数由调用者拼接。