深入扫描过程
使用gemmkit-tune调优讲的是如何运行工具。这一页讲的是扫描背后的机制,包括:
- 扫描测量什么
- 它如何裁定胜者
- 为什么它偏向出厂默认值
- 如何检验一份配置文件是否真的对你有帮助
一次只扫一个旋钮
扫描是一组彼此独立的一维搜索,而不是联合优化。对每个旋钮,其余所有旋钮都保持默认。工具把该旋钮的候选取值背靠背地测量,选出胜者,然后在扫描下一个旋钮之前把它恢复为默认。因此每个旋钮都是在一个除它以外全为默认的引擎上被评估的。
这是刻意的简化。对约三十个旋钮做完整的联合搜索在组合上毫无希望,而且会被噪声淹没。这些旋钮所把守的转折点在设计上就是各自独立地有意义,因此一维搜索正合适。
代价是不考察旋钮之间的相互作用,而这是可以接受的权衡,因为默认值本就落在一个良好的联合工作点上。工具的任务只是把各个转折点移到主机所在之处。
工具按固定的顺序测量候选,其中不含任何随机性。默认值排第一,因为它是 tie-break 的在位者,随后是各个不相同的额外候选。它为每个形状都以相同的种子重建缓冲区。因此两个候选取值之间的 A/B 对比看到的是逐字节相同的输入,任何机器漂移都会相互抵消。
扫描表与引擎旋钮注册表严格同步
gemmkit 在唯一一处枚举它的旋钮:gemmkit::tuning::knob_env_names()。这个机器可读的注册表是每一个 GEMMKIT_* 名字的唯一真相来源。调优器把每个旋钮归类为 TUNED(有真实的扫描)或 NEVER_TUNED(附理由),并有一个测试断言这两张表恰好划分 knob_env_names():不缺一个,也无陈旧项。
这条实际保证很直接:加进 gemmkit 的旋钮无法悄悄逃出自动调优器。在有人为它写好扫描、或记下为什么刻意放过它之前,构建都会失败。因此你读到工具列出的被扫描旋钮时,读到的是一张由编译器对照引擎保持诚实的列表。
测量什么,以什么单位
每个候选的得分是一种吞吐率。GEMM、i8 或 batched 探测以 GFLOP/s 计分,算法是每次调用 2*m*k*n,batched 再乘以 batch 数。gemv 探测则以 GB/s 计分,因为矩阵乘向量是带宽受限的,此处搬运的字节数才是诚实的评判标准。
单形状的估算刻意做得稳健。工具先预热探测闭包几次,再自动定出迭代次数,使一批计时约运行 50 毫秒。它计时若干这样的批次,并报告中位数速率,同时给出观测到的最小值和最大值。最小值和最大值不是装饰:它们记录的是逐次运行的离散度,而胜者逻辑正是用这个离散度在噪声下保持诚实。
打分:在一组探测形状上取几何平均
旋钮从不以单一形状评判。每个旋钮都携带一小组探测形状。工具挑选这些形状,为的是让该旋钮确实起作用,并从两侧夹住它的转折点。候选的得分随后就是它各形状中位吞吐率的几何平均。
几何平均不论绝对规模大小都给每个形状等权。因此单个大形状无法抬高一个只对它有利的取值。胜者必须是跨整组的普遍提升。最差形状的离散度会被带进几何平均,因此噪声闸门对整组保持保守,而非只信最平稳的那个形状。
探测按旋钮挑选,为的是让旋钮起作用。举几个例子:
| 旋钮 | 探测形状族 | 为什么是这些形状 |
|---|---|---|
MC_REG_PANELS | 方阵 f32,512 到 3072,并行 | 3072 那档压测 A 宏面板在 L2 中的驻留 |
LHS_PACK_THRESHOLD | 列主序 A,候选 32..MAX | 同时夹住 aarch64 的低复用平台段与 x86 的默认值 1024 |
SMALL_K_THRESHOLD | 瘦长的大 m,n 小 k,如 4096x16x4096 | k 跨越就地 / 打包驱动器的转折点 |
GEMV_PARALLEL_BYTES | 巨 m 的 gemv,GB/s | 覆盖缓存驻留 / DRAM 受限的字节下限 |
GEMV_TIER_STEP、GEMV_THREAD_CAP | 触碰约 2.4 到 134 MiB 的 gemv,GB/s | 跨越 gemv worker 阶梯的一级,因为整组都落在同一级里的探测集会把每个候选评成一样 |
SEQ_INTERNAL_BYTES_PER_WORKER(aarch64) | 让每 batch-worker 份额为 96/192/384/432 KiB 的 batched 形状 | 从两侧夹住约 128 KiB 的默认,是一个双向校验器 |
I8_VNNI_MIN_PAR_MNK(x86) | 方阵 i8,384/512/640 | 夹住 VNNI / 加宽回退的并行转折点 |
tie-break 偏向默认且感知噪声
取几何平均最高者是错的。在一台有噪声的机器上,1% 的领先通常只是运气。胜者逻辑改为从默认起步,只有当某个候选的几何平均超过当前最优、且超出两个候选中较大的那份实测离散度时,才会升级到它。逐次运行的噪声按构造无法越过这道门槛,因此永远无法改写一个旋钮。恰好打平则保留默认。
对默认为 0 的那些 “auto” 旋钮还有一道额外余量。这些旋钮从机器派生取值,例如 LLC 大小、核心数、页大小。固定候选必须在噪声之外再多赢 auto 5%。那些 auto 派生会适应探测集未覆盖的形状,所以一个在探测上仅以毫厘取胜的固定数,不值得拿这份适应性去换。
这种偏向默认在噪声下是正确取舍。默认是一个已知良好、经过刻意选择的取值,而工具常常在无人盯着的机器上无人值守地运行。这道不对称的门槛意味着最坏情况不过是工具重现了默认值,它绝不会把你退化进一个测量假象里。
扫描完全没有 RNG,因此一次运行值得信任:最坏什么都不做,而它移动某个旋钮时,一定是因为一个真实、可复现的提升越过了噪声。
时间预算如何封顶并放粗扫描
--time-budget 从两方面起作用。
其一,它预先放粗每次估算:无预算 7 次计时重复,90 秒以下 5 次,30 秒以下 3 次,以一点测量稳定性换速度。
其二,它强制一个硬截止。每个旋钮之前工具都查看时钟,一旦超过截止时刻,就不再开始新的扫描,把每个剩余旋钮记为因 “time budget exhausted” 而跳过。
因此紧预算既会模糊它确实做的那些测量,又会从尾部丢弃旋钮。无预算时扫描以全额重复次数跑到底。
哪些旋钮被跳过,以及为什么
有些旋钮从不扫描,报告和配置文件尾注会逐个说明原因:
PARALLEL_THRESHOLD:串行/并行的收支平衡点强烈依赖形状。单个m*n*k标量无法适配所有长宽比,因此工具保留经过校准的跨形状默认,而不去自动拟合它。对比GEMV_THRESHOLD,它是干净的二元开/关决策,会被扫描。DEEP_KC_BYTES:它把守 f16/bf16 深度收缩孪生路径,而调优器不跑窄类型探测。其 auto 默认从 L2(一项机器属性)派生。若需重调窄类型的深k触发点,请直接覆盖它。PREFETCH_MIN_BYTES:它把守驱动器的 C tile 预取。其 auto 默认从检测到的 LLC(一项机器属性)派生,而探测这个转折点需要每个候选都用超出 LLC 的工作集。请直接覆盖它以重调触发点(usize::MAX关闭预取,1强制开启)。
另一些旋钮在当前目标上是惰性的,因而以此为由跳过。SEQ_INTERNAL_BYTES_PER_WORKER 只被 aarch64 的 batched 拆分规划器读取:在那里扫描,在 x86 上惰性并跳过。I8_VNNI_MIN_PAR_MNK 把守的是 x86 VNNI 小并行回退,其他目标的 i8 内核并无此回退。NC_NO_L3_PANELS 只在没有 L3 的机器上被查阅:在那里扫描,在有 L3 的主机上惰性并跳过。
两个重型旋钮,除非你传入 --large-matrices,否则一律跳过。
–large-matrices 解锁什么
有两个旋钮只在一个复现起来昂贵的区间才重要,所以它们藏在内存预算之后、需显式开启。
K_STREAM_MAX 限定 axpy-gemv 的输出保持寄存器分块到多远。它只有当输出明确 DRAM 受限时才占优。因此它的探测把输出固定在约两倍末级缓存。1x-LLC 的输出正落在缓存边界上,测不出任何决定性结果,所以探测避开这个尺寸。探测随后在校准上限附近扫描 k。
那个输出尺寸是固定的,不随预算缩放,因此达到它要用数 GB 的矩阵。如果你给的预算装不下最大的探测,工具会跳过该旋钮。它会打印出应当重跑时使用的 GiB 数(向上取整)。在 32 位目标上,它会直接跳过该旋钮,因为那些矩阵根本装不进地址空间。
SHARED_LHS_MNK 把守共享 LHS 预打包。这个预打包消除每个 worker 冗余的 A 打包,却增加一道 fork-join 屏障,因此只在越过一个较大的 m*n*k 取值(x86 上约 8e9)后才划算。它的探测用的是越过该转折点的瘦高、极高 FLOP 形状。
工具在普通扫描期间会中和这两个旋钮,无论自身是否在扫描它们。这样陈旧的环境变量取值就无法扭曲读取它们的基准。
读懂终端报告
报告开头是一张每旋钮一行的汇总表。它的列是旋钮、单位、形状数、默认、胜者、加速比,以及一个读作 keeps default 或 -> <value> 的结果列,移动过的旋钮会高亮。
表格下方,一段候选明细为每个旋钮打印完整的扫描地形。它列出每个候选的几何平均中位数,用一个前置标记标出默认取值,用另一个标记标出胜者,让你看出最优点有多平坦或多陡峭。
再往下是带原因的跳过列表,然后是一段页脚。页脚计出扫描了多少旋钮、多少移离默认、多少被跳过。在参考机器上,页脚会注明所有旋钮都保持了默认,这是意料之中的,配置文件会重现它们。
校验一份配置文件
扫描测量的是合成的、大致方形的探测。这对找出一台机器的转折点是正确选择。但你的工作负载有自己的形状,所以在生产中信任一份配置文件之前,先确认收益能迁移过去。
有两种检验方式。直接的一种:在部署主机上,分别在 source 与不 source gemmkit-tune.env 的情况下对你自己的应用计时并比较。可复现的一种:运行 gemmkit 的 criterion 基准,它覆盖五个头部分组(sgemm、dtypes、gemv、prepacked、batched),并以一个已保存的基线对比:
cargo bench -p gemmkit -- --save-baseline stock
source gemmkit-tune.env
cargo bench -p gemmkit -- --baseline stock
如果某个旋钮移动了而你在意的东西反倒退化,配置文件就是一个纯文本文件。删掉或注释掉那一行 export,其余照留。头部标注和每行的 tuned/default 标记让你很容易看出该动哪一行。