体系结构驱动优化:从性能证据到算法、数据与并行处理 / Architecture-Driven Optimization from Evidence to Algorithms and Parallelism
📅 创建时间:2026-07-23
🏷️ 标签:#性能优化 #Roofline #CPU #GPU #MPI #数值算法
📚 前置知识:[[05-cae-performance-analysis]]
📚 相关知识:[[../marine-cae/12-serial-and-algorithmic-acceleration]] [[../marine-cae/13-parallel-acceleration-map]]
1. 优化优先级
推荐顺序:
- 删除不必要的物理、网格、时间步和输出工作;
- 改善离散方法、求解算法和预条件器;
- 减少数据规模、拷贝和格式转换;
- 改善数据布局、局部性和任务粒度;
- 并行化主要热点并处理负载均衡;
- 重叠计算、通信和 I/O;
- 最后处理 SIMD、Kernel Fusion 和指令级细节。
这个顺序不是绝对规则,但能防止花大量时间优化只占总时间 2% 的循环。
2. 先区分两类慢
单次迭代慢
可能来自:
- SpMV、组装或通量 Kernel 低效;
- Cache、带宽、NUMA;
- GPU 传输与同步;
- MPI 通信;
- I/O。
迭代次数多
可能来自:
- 条件数差;
- 网格质量差;
- 离散格式或变量尺度;
- 预条件器不合适;
- 非线性切线不一致;
- 时间步、松弛或耦合策略。
总时间 = 迭代次数 × 单次迭代成本只优化其中一项可能被另一项抵消。例如较便宜但更弱的预条件器可能让总时间反而增加。
3. 建立可证伪假设
好的假设:
perf 和 PCM 显示 SpMV 已接近实际内存带宽,values、colIdx 和 x 读取占主导;将节点三自由度 CSR 改为 3×3 BSR 应减少索引字节并改善连续访问。
不好的假设:
GPU 应该更快,试着改成 CUDA。
每个假设要说明:
- 证据;
- 推测的体系结构机制;
- 唯一主要修改;
- 预期影响的指标;
- 正确性风险;
- 如果失败,怎样推翻假设。
4. CPU:IPC 低
IPC 低可能意味着:
- 等待内存;
- 分支预测失败;
- 指令前端供给不足;
- 长延迟除法、开方或依赖链;
- 锁和同步;
- 运行频率或线程调度问题。
处理顺序:
- 用 Top-Down 或计数器区分前端、推测和后端;
- 若 Memory Bound,继续检查 Cache、DRAM 和 NUMA;
- 若 Branch Bound,检查边界/材料/单元类型混合;
- 若 Core Bound,检查向量化、指令组合和依赖;
- 若等待,检查锁、Barrier 和任务划分。
不能仅凭 IPC 低就重写算法。
5. CPU:Cache Miss 和间接访存
常见于:
- CSR 的 x[colIdx];
- CFD owner/neighbour;
- 有限元单元节点 Gather;
- 接触邻居和搜索结构。
可能处理:
- 网格和自由度重编号;
- 按空间或分区分块;
- 将单元、Face 按类型和拓扑分组;
- 批量处理多个场,复用已加载邻居;
- 压缩索引;
- 软件预取,但必须测量;
- 对只用一部分分量的循环改为 SoA。
重编号可能改善 Cache,却恶化 Fill-in 或 MPI 边界,因此要测端到端。
6. CPU:内存带宽饱和
证据:
- PCM/LIKWID 显示带宽接近平台实际峰值;
- 增加线程后带宽不再增长,时间也不再下降;
- FLOPS 很低但 CPU 利用率高;
- Roofline 位于带宽斜线附近。
处理:
- 减少索引和数值字节;
- CSR 转 BSR/SELL;
- 合并多个向量循环;
- 避免临时数组和重复读写;
- Matrix-Free 用计算换流量;
- 降低精度,但必须验证;
- 在算法层减少 SpMV 次数;
- 使用更强预条件器减少迭代。
带宽饱和时继续增加线程通常只增加能耗。
7. CPU:分支和 SIMD
结构/CFD 中常见分支:
- 单元或边界类型;
- 材料状态;
- 限制器;
- 接触活动状态;
- 湍流壁面处理。
处理:
- 按类型分桶后调用专用 Kernel;
- 将常量条件移出循环;
- 使用 Mask 处理短分支;
- 对固定小矩阵模板化;
- 分离内部面和边界面;
- 为常见路径提供快路径。
分桶会增加预处理和数据重排,适合重复迭代较多的算例。
8. CPU:False Sharing、锁与原子
证据:
- Threading Profiler 显示锁或原子时间高;
- 增加线程后 Cache 一致性流量上升;
- 图着色后扩展性显著变化;
- 线程完成时间差异大。
处理:
- 线程私有缓冲;
- 分区所有权;
- 图着色;
- COO 生成后归并;
- Padding 高频计数器;
- 批量归约;
- 减少细粒度任务和共享队列竞争。
原子并非一定慢。冲突概率低时,它可能比复杂着色更好。
9. NUMA
证据:
- numastat 显示大量远端页;
- 单 Socket 快,跨 Socket 扩展差;
- 绑定后性能明显变化;
- 内存带宽集中在一个 Socket。
处理:
- 并行 First Touch;
- 线程绑核;
- 每 NUMA 节点一个 MPI Rank;
- 数据按子域分配;
- 避免主线程串行初始化大数组;
- 将线程私有 Scratch 放在本地节点。
需要同时检查 MPI 通信增加是否抵消 NUMA 收益。
10. GPU:启动开销和小 Kernel
时间线出现大量短 Kernel 和空洞时:
- 合并连续向量操作;
- 批量处理多个单元、方程或边界;
- 使用 CUDA Graph 减少重复启动开销;
- 将收敛统计留在设备端;
- 避免每个小阶段都同步主机;
- 对多算例使用批处理。
融合过度会增加寄存器、降低 Occupancy,并让代码难维护。以端到端时间决定是否保留。
11. GPU:数据传输和驻留
证据:
- H2D/D2H 占比高;
- 每次迭代都有相同数组拷贝;
- GPU Kernel 快但总时间不快;
- 隐式 Unified Memory 缺页。
处理:
- 主迭代数据常驻设备;
- 只在输出或 Checkpoint 时下载必要变量;
- 使用异步拷贝和双缓冲;
- 把预处理或结果恢复也迁到设备;
- 缓存矩阵和预条件数据;
- 用增量通信代替完整字段复制。
数据所有权必须清晰,避免 Host 和 Device 两份状态彼此覆盖。
12. GPU:访存、分支和原子
不合并访存
用 SoA、重排、连续 Face/Cell 分组和块格式改善。
分支发散
按边界、材料、单元或活动状态分桶。检查分桶成本和批次大小。
原子冲突
有限元组装或 Face 对 Cell 散射可尝试:
- 图着色;
- owner/neighbour 分阶段;
- COO + sort/reduce;
- Block 私有累加;
- 改为 Gather 形式;
- Matrix-Free。
归约
Krylov 点积和残差范数需要归约。设备内使用分层归约,MPI 场景进一步考虑合并或流水化全局归约。
13. MPI:负载不均
证据:
- 最慢 Rank 与平均 Rank 差异大;
- Barrier/Allreduce 中大量等待;
- 单元数相近但物理工作量不同;
- 接触、化学或湍流模型集中在少数分区。
处理:
- 使用计算权重而不是只按单元数分区;
- 动态或阶段性重分区;
- 将接触面、粒子和源项计入权重;
- 平衡 AMG 粗层;
- 避免某个 Rank 独占串行服务;
- 输出每 Rank 工作量直方图。
重分区有迁移成本,适合长时间、多步计算。
14. MPI:通信占比高
小消息过多
合并字段和邻居消息,复用通信计划,避免每个变量单独交换。
Halo 占主导
增加每 Rank 工作量、改善分区表面积/体积比、使用高质量图分区,或停止继续强扩展。
全局归约多
尝试:
- 合并多个统计量;
- Pipelined CG/GMRES;
- Communication-Avoiding 算法;
- 降低不必要的逐迭代全局日志;
- 在允许时减少收敛检查频率。
算法变体可能改变稳定性和舍入行为,需要单独验证。
计算通信重叠
启动 Irecv/Isend
→ 计算内部区域
→ 等待
→ 计算边界区域只有内部工作时间足以覆盖通信,且 MPI 能异步推进时才有效。
15. 线性求解器与预条件器
直接法
优化方向:
- 重排序减少 Fill-in;
- 复用符号分析;
- 多右端项复用数值因子;
- 使用块结构;
- 控制并行线程和 NUMA;
- 估计内存后再选择算法。
Krylov
总成本:
迭代数 ×(SpMV + 预条件 + 向量操作 + 全局归约)AMG
需要分开测:
- Setup;
- 每次 Apply;
- 层数和粗网格大小;
- 各层 SpMV、平滑和通信;
- 迭代减少收益。
短算例中昂贵 Setup 可能不划算;长算例或多右端项中复用非常重要。
ILU/IC
天然存在三角依赖。并行方法包括 Level Scheduling、Block ILU、Additive Schwarz 和近似逆。更高并行度可能降低预条件质量。
16. 离散和模型层优化
在硬件优化前检查:
- 网格是否在不重要区域过密;
- 是否能使用自适应网格;
- 稳态问题是否错误地长时间瞬态推进;
- 载荷或参数扫描是否能复用矩阵和预条件器;
- 是否能用延续法改善非线性;
- 多工况是否可并行;
- 输出字段和频率是否必要;
- 是否适合模态降阶、POD/ROM。
减少一个数量级自由度通常比微调 Kernel 更有效,但不能牺牲关键物理分辨率。
17. 混合精度
可选策略:
- Field 或几何量使用 float,累加使用 double;
- 预条件器低精度,外层 Krylov 高精度;
- 直接法低精度分解加迭代改进;
- GPU Tensor/Core 混合精度用于规则块。
必须检查:
- 条件数;
- 残差是否停滞;
- 守恒和平衡;
- 非线性路径;
- 不同规模和工况;
- 并行归约误差。
混合精度是数值算法设计,不是简单替换类型。
18. I/O
证据:
- 输出阶段占比高;
- 大量小写;
- 所有 Rank 争用同一文件;
- Checkpoint 周期产生 I/O 峰值;
- 元数据操作多。
处理:
- 降低输出频率和变量数量;
- 使用块写和压缩;
- Parallel HDF5/ADIOS2/MPI-IO;
- 集合缓冲或 I/O Rank;
- 异步写出;
- 将可重算派生量留到后处理;
- 错开多个作业 Checkpoint。
压缩会增加 CPU 成本,需比较总时间和存储成本。
19. 观察、解释与下一步
| 观察到什么 | 说明什么 | 下一步检查什么 |
|---|---|---|
| 迭代数异常高 | 数值算法或问题条件优先于硬件优化 | 网格、尺度、离散和预条件对比 |
| SpMV 接近带宽上限 | 单次算子受数据移动限制 | 索引字节、块格式、融合和 Matrix-Free |
| LLC Miss 高 | 邻接访问和工作集局部性较差 | 重编号、分块、布局和复用 |
| 分支失败高 | 热循环混合了不同执行路径 | 类型分桶、快路径和 Mask 效率 |
| 跨 Socket 扩展差 | NUMA 页和线程映射可能不匹配 | First Touch、远端访问和 Rank 映射 |
| GPU 时间线有大空洞 | 同步、主机准备或粒度限制设备 | CUDA API、CPU 栈和 Kernel 批次 |
| GPU 拷贝占比高 | 数据未常驻或所有权不清晰 | 传输内容、频率和异步流水 |
| Rank 等待差异大 | 负载或节点性能存在长尾 | 加权分区、最慢 Rank 和 Straggler |
| Allreduce 占比高 | 强扩展进入延迟与同步区 | 归约次数、流水化算法和停止规模 |
| 输出占比高 | I/O 而非求解器 Kernel 主导 | 字段/频率、并行 I/O 和异步化 |
20. 何时停止优化
出现以下情况应考虑停止:
- 热点已接近 Roofline 或平台实际带宽;
- 优化对象占端到端时间很小;
- 加速收益小于代码复杂度和维护成本;
- 数值稳定性或可重复性风险增加;
- 目标硬件即将变化;
- 更大收益需要改变业务需求或物理模型;
- 并行规模已超过合理强扩展点。
最终目标是更快、更稳、更便宜地得到可信工程结论,而不是追求某个指标的极值。
下一篇:[[07-solver-optimization-case-studies]]