理论峰值
3.5 GHz × 4 (FMA) × 2 (FLOP/FMA) × 2 (流水线) = 56 GFlop/s
你只能修改 dgemm-blocked.c 中的 square_dgemm() 函数,目标是在 Perlmutter 的单核上逼近理论峰值 56 GFlop/s。
理论峰值
3.5 GHz × 4 (FMA) × 2 (FLOP/FMA) × 2 (流水线) = 56 GFlop/s
运算下界
不能少于 2n³ 次浮点运算(Strassen 可选,但不推荐)
存储顺序
矩阵按列主序(Column-major)存储,注意遍历顺序
正确性验证
|square_dgemm(n,A,B,0) − A×B| < ε·n·|A|·|B|
| 文件 / 函数 | 角色 | 可以修改? | 课程关联 |
|---|---|---|---|
| dgemm-blocked.c → square_dgemm() | 你唯一的战场:实现优化的矩阵乘法 | ✅ 是 | Lec 2, 3 |
| dgemm-bench.c | 计时与 Roofline 测量框架 | ❌ 否 | Lec 3 |
| dgemm-ref.c | 参考实现,用于正确性对比 | ❌ 否 | — |
| Makefile / CMakeLists.txt | 编译与打包配置 | ⚠️ 谨慎 | 工具链 |
先用 dgemm-bench 跑参考实现,观察当前 GFlop/s 与 Roofline 的关系。Perlmutter 节点的计算峰值 56 GFlop/s,L1/L2/DRAM 带宽决定了你的上限。问自己:这个矩阵是 compute-bound 还是 memory-bound?
把大矩阵切成小块,让数据在 Cache 中驻留。这是从 O(n³) 内存流量 → O(n³/√M) 的关键一步。
把每个 Block 再切成 4×4、6×6 的小核,让 CPU 寄存器直接累加。手写 AVX2 内联函数(_mm256_fmadd_pd)或内联汇编。
__restrict__ 帮助编译器别名分析当数据还在 L2 时就用 _mm_prefetch 提前拉入 L1,隐藏内存延迟。
把 Block 从主存拷贝到连续的局部缓冲区,减少 TLB miss 并改善向量化对齐。
# 在 Perlmutter 上编译
module load gcc/12.3
mkdir build && cd build
cmake -DGROUP_NO=NN ..
make package
# 提交生成的 .tar.gz 文件
cmake -DGROUP_NO=NN .. 指定组号,否则无法打包。报告最多 8 页,包含性能图、Roofline 分析和优化解释。
| 部分 | 关键内容 | 页数建议 |
|---|---|---|
| Roofline 图 | 每个优化阶段的 Roofline 点,标注分阶段提升 | 1 页 |
| 性能曲线 | GFlop/s vs 矩阵大小 n,对比参考实现 | 1 页 |
| 优化方法论 | 每步优化做了什么、为什么有效、数据支撑 | 3-4 页 |
| 挑战与反思 | 瓶颈在哪、为何未达到峰值 | 1-2 页 |
Q1. 在列主序存储中,如何遍历矩阵才能获得最佳的内存局部性?
Q2. Perlmutter 节点单核的理论峰值 56 GFlop/s 是如何计算的?
Q3. 为什么 Cache Blocking 能显著提升矩阵乘法性能?
Q4. 关于 Strassen 算法,以下哪项正确?
Q5. 软件预取 (Software Prefetching) 的最佳时机是?