CS267 · Homework 1 导读

矩阵乘法优化

在 NERSC Perlmutter 单核上压榨 dgemm 的每一 FLOP——从内存层次到 Roofline,从分块到 AVX 内联函数。

📅 截止:2025-02-11 📖 Lecture 1-3 💻 C (GCC 12.3) 🏆 正确性 + 性能 + 解释

与课程的关系

Lecture 1 · 导论
↓
并行计算基础、性能度量
Lecture 2 · 内存层次
↓
Cache 分块、局部性原理
Lecture 3 · Roofline
↓
计算/内存瓶颈分析
Lecture · SIMD
↓
AVX2 向量化指令

任务概述

你只能修改 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 编译与打包配置 ⚠️ 谨慎 工具链

优化路线(Step by Step)

1

理解 Roofline 与瓶颈

先用 dgemm-bench 跑参考实现,观察当前 GFlop/s 与 Roofline 的关系。Perlmutter 节点的计算峰值 56 GFlop/s,L1/L2/DRAM 带宽决定了你的上限。问自己:这个矩阵是 compute-bound 还是 memory-bound?

2

Cache 分块 (Blocking / Tiling)

把大矩阵切成小块,让数据在 Cache 中驻留。这是从 O(n³) 内存流量 → O(n³/√M) 的关键一步。

⚠️ 常见坑:不要假设行主序!列主序意味着内层循环应遍历行索引,以获得连续内存访问(stride-1)。
3

寄存器分块内核 (Register Blocking)

把每个 Block 再切成 4×4、6×6 的小核,让 CPU 寄存器直接累加。手写 AVX2 内联函数(_mm256_fmadd_pd)或内联汇编。

4

手动预取 (Software Prefetching)

当数据还在 L2 时就用 _mm_prefetch 提前拉入 L1,隐藏内存延迟。

5

打包 (Packing / Copy Optimization)

把 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 分析和优化解释。

这不是"写一个矩阵乘法",而是"理解为什么你的 CPU 跑不到峰值"。 每一步优化都要有 Roofline 数据支撑——不要盲调参数,要让数据告诉你下一步该优化什么。


报告应包含什么

部分 关键内容 页数建议
Roofline 图 每个优化阶段的 Roofline 点,标注分阶段提升 1 页
性能曲线 GFlop/s vs 矩阵大小 n,对比参考实现 1 页
优化方法论 每步优化做了什么、为什么有效、数据支撑 3-4 页
挑战与反思 瓶颈在哪、为何未达到峰值 1-2 页

自测 Quiz

Q1. 在列主序存储中,如何遍历矩阵才能获得最佳的内存局部性?

Q2. Perlmutter 节点单核的理论峰值 56 GFlop/s 是如何计算的?

Q3. 为什么 Cache Blocking 能显著提升矩阵乘法性能?

Q4. 关于 Strassen 算法,以下哪项正确?

Q5. 软件预取 (Software Prefetching) 的最佳时机是?