求解器优化案例:结构算子、CFD 通量与 MPI 强扩展 / Solver Optimization Cases for Structural Kernels, CFD Fluxes, and MPI Scaling
📅 创建时间:2026-07-23
🏷️ 标签:#性能案例 #SpMV #CFD通量 #MPI扩展 #Benchmark
📚 前置知识:[[06-architecture-driven-optimization]]
📚 相关知识:[[/06-other/01-projects/profile/C++/2-StructKernelBench/2-StructKernelBench]] [[../marine-cae/15-verification-and-validation]]
1. 案例的统一写法
一个可信的优化案例必须包含:
场景和工程目标
输入规模和硬件环境
正确性标准
端到端基线
阶段和热点证据
可证伪假设
唯一主要修改
优化后测量
迭代次数和工程结果检查
结论与下一实验下面数值用于演示分析方法,不代表某个商业软件的实测结果。实际项目必须保存原始日志和命令。
2. 案例一:结构求解中的 CSR SpMV
2.1 场景
三维线弹性模型:
- 500 万自由度;
- 每个节点 3 个位移自由度;
- CG + AMG;
- 线性求解占端到端时间 76%;
- SpMV 占线性求解时间 44%。
目标是在不改变迭代次数和位移结果的前提下降低单次 SpMV 时间。
2.2 基线
示例数据:
| 指标 | 基线 |
|---|---|
| 总求解时间 | 100 s |
| CG 迭代数 | 420 |
| SpMV 总时间 | 33.4 s |
| 内存带宽 | 165 GB/s |
| 平台 STREAM 带宽 | 190 GB/s |
| CPU IPC | 0.72 |
| 32 线程相对 16 线程加速 | 1.05× |
perf 显示 LLC Miss 高,PCM 显示带宽接近实际峰值。线程增加后时间不再下降,说明热点主要受带宽限制,而不是缺少核心。
2.3 假设
矩阵来自三维位移问题,存在大量 3×3 节点块。CSR 为每个标量非零元保存列索引,索引流量较大。
假设:
转换为 3×3 BSR 可以让九个数值共享一次块列索引,并提高 x 向量连续访问,从而降低每次 SpMV 的字节数。
2.4 修改
- 保持自由度编号和 CG/AMG 参数不变;
- 在矩阵构造阶段生成 3×3 BSR;
- 对约束行使用明确的块内处理;
- 添加 CSR 与 BSR 两个后端;
- 使用相同初值和停止准则。
2.5 结果
示例:
| 指标 | CSR | BSR |
|---|---|---|
| CG 迭代数 | 420 | 420 |
| SpMV 总时间 | 33.4 s | 23.8 s |
| 总求解时间 | 100 s | 90.2 s |
| 峰值矩阵内存 | 8.6 GB | 7.4 GB |
| 最大位移差 | — | 2.1×10⁻¹³ |
SpMV 提升约 1.40×,端到端只提升约 1.11×,符合 Amdahl 定律。不能宣传“求解器提升 40%”。
2.6 进一步实验
- 节点重编号是否改善 x 访问;
- AMG 是否能直接使用块平滑器;
- 约束较多或混合单元时 BSR 浪费多少;
- 32 位块索引是否满足规模;
- GPU BSR 与 CSR 哪个更适合实际行结构。
3. 案例二:应力恢复的 AoS 到 SoA
3.1 场景
位移求解后,需要为大量积分点计算:
- 六分量应力;
- von Mises;
- 多工况最大值。
该阶段数据并行度高,适合作为 [[/06-other/01-projects/profile/C++/2-StructKernelBench/2-StructKernelBench]] 的实践。
3.2 环境与基线
示例实验固定:
CPU:双路 32 核,固定频率并绑定线程
构建:Clang Release,-O3,关闭 Fast-Math
输入:2,000 万积分点,六分量双精度应力
线程:1、8、16、32,使用相同静态分块
正确性:逐点相对误差 ≤ 1×10⁻¹²,包络索引完全一致| 指标 | AoS 基线 |
|---|---|
| 应力恢复阶段 | 2.80 s |
| von Mises 循环 | 1.71 s |
| 32 线程相对单线程加速 | 9.4× |
| 实际内存带宽 | 118 GB/s |
| 向量化 Lane 利用率 | 43% |
3.3 证据
编译器报告显示 AoS 版本未充分向量化,因为循环只计算 von Mises,却以结构体形式读取全部字段并存在别名顾虑。
Advisor 显示:
- Vectorization Efficiency 低;
- 内存访问连续但每个点对象较大;
- 该循环占后处理时间 61%。
3.4 修改
AoS: stress[i].xx, stress[i].yy, ...
→
SoA: xx[i], yy[i], zz[i], xy[i], yz[i], zx[i]同时:
- 输出数组连续;
- 外层按大块 OpenMP;
- 避免循环内分配;
- 为尾部使用编译器 Mask;
- 保留双精度累加。
3.5 结果与验证
示例结果:
| 指标 | AoS | SoA |
|---|---|---|
| von Mises 循环 | 1.71 s | 0.96 s |
| 应力恢复阶段 | 2.80 s | 2.02 s |
| 32 线程相对单线程加速 | 9.4× | 13.8× |
| 向量化 Lane 利用率 | 43% | 86% |
| 最大逐点相对误差 | — | 3.7×10⁻¹⁴ |
| 包络索引差异数 | — | 0 |
同时检查:
- 每点 von Mises 的最大绝对/相对误差;
- NaN、Inf 和极端应力;
- 多工况包络索引;
- 单线程、不同线程数下结果;
- 小规模和超出 LLC 的大规模。
3.6 结论
如果小规模加速明显、大规模加速变小,可能是大规模开始受内存带宽限制。下一步应测带宽,而不是继续增加 SIMD 指令。
4. 案例三:CFD 面通量循环
4.1 场景
非结构网格有限体积求解器中,面循环:
for (Face f : internalFaces) {
Cell o = owner[f];
Cell n = neighbour[f];
Flux flux = evaluate(state[o], state[n], geometry[f]);
residual[o] += flux;
residual[n] -= flux;
}Nsight/CPU Profiler 显示它是主要热点之一。
4.2 环境与基线
示例实验:
硬件:单路 32 核 CPU,256 GB 内存
构建:GCC Release,-O3,OpenMP
网格:2,400 万 Cell,7,100 万内部 Face
算法:稳态 SIMPLE,固定松弛和线性容差
正确性:质量不平衡 ≤ 1×10⁻⁶,阻力系数相对差 ≤ 1×10⁻⁸| 指标 | 基线 |
|---|---|
| 平均每次 SIMPLE 外迭代 | 4.80 s |
| 面通量循环 | 1.60 s |
| 压力求解 | 2.64 s |
| 分支预测失败率 | 8.1% |
| LLC Miss Rate | 31% |
| 收敛所需外迭代 | 1,260 |
4.3 初始证据
- owner/neighbour 导致间接读取;
- 对 residual 的双向散射产生并行写冲突;
- 多种离散格式和限制器在循环内分支;
- Face 顺序与 Cell 空间顺序无关;
- 分支失败、LLC Miss 和原子冲突均较高。
4.4 不应一次做完的修改
以下修改若同时进行,就无法判断收益来源:
- Face 重排;
- AoS 转 SoA;
- 分离边界类型;
- 原子改着色;
- Kernel Fusion;
- 精度变化。
应逐个建立实验。
4.5 实验 A:内部面与边界面分离
目的:删除主内部面循环中的边界判断。
验证:
- 所有边界仍被恰好处理一次;
- 总质量通量不变;
- 残差逐 Cell 等价;
- 分支失败和循环时间变化。
如果分支下降但总时间不变,说明主要瓶颈可能仍是内存或原子。
4.6 实验 B:按 owner/空间局部性重排 Face
目的:让连续 Face 更可能复用 owner Cell 数据。
同时测:
- L2/LLC Hit;
- residual 写入局部性;
- neighbour 访问是否恶化;
- MPI 内部面/边界面分组是否受影响;
- 梯度等其他面循环是否受益。
4.7 实验 C:原子与图着色
比较:
| 方案 | 关注点 |
|---|---|
| 原子 | 冲突率、实现简单、颜色预处理为零 |
| 图着色 | 同色无冲突、颜色同步、重排成本 |
| Cell Gather | 无散射冲突、需要 Cell-Face 邻接 |
| 局部缓冲归并 | 内存量和归并时间 |
稳态数千次迭代时,着色预处理容易摊销;一次性计算或网格频繁变化时不一定合适。
4.8 结果与数值验证
依次实施“内部/边界分离”和“局部 Face 重排”,没有在同一实验中切换组装策略:
| 指标 | 基线 | 优化后 |
|---|---|---|
| 面通量循环 | 1.60 s | 1.05 s |
| 平均每次 SIMPLE 外迭代 | 4.80 s | 4.21 s |
| 分支预测失败率 | 8.1% | 2.7% |
| LLC Miss Rate | 31% | 22% |
| 收敛所需外迭代 | 1,260 | 1,260 |
| 最终质量不平衡 | 4.2×10⁻⁷ | 4.2×10⁻⁷ |
| 阻力系数相对差 | — | 6.1×10⁻¹⁰ |
另外逐 Cell 比较一次固定状态下的通量和残差,并确认:
- 每个内部 Face 恰好贡献给 owner 和 neighbour;
- 每个边界 Face 恰好处理一次;
- 重排前后的残差范数和守恒量在双精度容差内一致;
- 相同停止准则下残差历史没有系统性改变。
4.9 端到端限制
假设通量循环快了 1.8×,但压力 AMG 仍占总时间 55%,端到端收益会受限。下一步应检查压力迭代数和 AMG,而不是继续深挖通量 Kernel。
5. 案例四:MPI 强扩展停止
5.1 场景
固定 2 亿 Cell 的 CFD 算例,从 64 Rank 扩展到 1024 Rank。
实验使用同一批计算节点、同一 MPI 和 Release 二进制;每节点 64 个 CPU 核,每个 Rank 固定一个核心集合。分区器版本、SIMPLE/AMG 参数、线性与外层停止准则保持不变。正确性标准为质量不平衡不超过 1×10⁻⁶、阻力系数相对 64 Rank 结果差异不超过 1×10⁻⁷。
示例数据:
| Rank | 时间/步 | 并行效率(相对 64) | MPI 占比 |
|---|---|---|---|
| 64 | 8.0 s | 100% | 12% |
| 128 | 4.3 s | 93% | 17% |
| 256 | 2.4 s | 83% | 26% |
| 512 | 1.6 s | 63% | 41% |
| 1024 | 1.35 s | 37% | 59% |
1024 Rank 比 512 Rank 只快 1.19×,资源却增加一倍。
5.2 分解
Score-P/Scalasca 显示:
- Halo 消息数不变但每条消息更小;
- Allreduce 延迟占比上升;
- AMG 粗层大量 Rank 没有足够工作;
- 最慢 Rank 的边界 Face 数明显高于平均;
- 输出步骤出现共享文件系统等待。
5.3 优化假设
按优先级:
- 重新分区,权重加入边界面和模型计算量;
- 合并多个 Field 的 Halo;
- 内部区域计算与 Halo 交换重叠;
- 减少或流水化 Krylov 全局归约;
- AMG 粗层收缩活动 Rank;
- 调整并行 I/O;
- 判断 512 Rank 是否已是经济停止点。
5.4 重叠实验
Irecv/Isend
→ 内部 Cell/Face
→ Waitall
→ 分区边界 Cell/Face测量不能只看 MPI_Waitall 下降,还要看:
- 内部/边界拆分额外开销;
- 总步骤时间;
- MPI 是否有异步进展;
- GPU 场景是否需要 CUDA-aware MPI;
- Rank 不均是否仍决定总时间。
5.5 优化后结果与数值验证
示例中采用加权重分区、Halo 字段合并和 AMG 粗层活动 Rank 收缩:
| Rank | 优化前时间/步 | 优化后时间/步 | 优化后 MPI 占比 |
|---|---|---|---|
| 512 | 1.60 s | 1.42 s | 34% |
| 1024 | 1.35 s | 1.08 s | 46% |
验证结果:
| 检查 | 结果 |
|---|---|
| 1024 Rank 质量不平衡 | 5.0×10⁻⁷ |
| 相对 64 Rank 阻力系数差 | 2.8×10⁻⁹ |
| 压力方程平均迭代数差 | 0 |
| 最终外迭代数差 | 0 |
| 丢失或重复 Halo 实体 | 0 |
还应比较各 Rank 的局部守恒和输出字段校验和,防止只看全局量时掩盖通信索引错误。
5.6 合理停止
如果优化后 1024 Rank 的成本/步仍显著高于 512 Rank,且目标周转时间已满足,应选择 512 Rank。强扩展不是越大越好。
6. 案例五:迭代次数而不是 Kernel
6.1 现象
新网格下总时间翻倍。Profiler 显示 SpMV 单次时间几乎不变,但压力方程迭代从 40 次增至 115 次。
6.2 错误方向
- 改写 SpMV;
- 增加 GPU;
- 关闭性能计时;
- 降低收敛标准。
6.3 正确检查
- 网格非正交和长宽比;
- 压力参考和边界;
- 系数尺度;
- AMG Coarsening 和 Smoother;
- 非正交修正次数;
- 分区导致的 AMG 质量变化;
- 初值和时间步。
如果更强 AMG 让单次迭代贵 20%,但迭代从 115 降到 45,总时间仍会显著下降。这说明算法质量高于微观 Kernel 指标。
7. “求解器很慢”的决策树
求解器很慢
│
├─ 输入、初始化或输出占主导?
│ ├─ 是 → 文件数量、格式、并行 I/O、输出频率
│ └─ 否
│
├─ 迭代/时间步/增量数量异常?
│ ├─ 是 → 网格、模型、离散、预条件、步长和收敛
│ └─ 否
│
├─ 单次迭代哪个阶段最大?
│ ├─ 组装/通量 → 数据布局、冲突、SIMD/GPU
│ ├─ 线性求解 → 矩阵性质、SpMV、预条件、归约
│ ├─ 通信 → 分区、消息、重叠、强扩展点
│ └─ I/O → 并行格式、批量写、异步
│
├─ CPU、GPU 或 MPI 证据是什么?
│ └─ 用对应工具验证体系结构机制
│
└─ 修改后
├─ 重新测量端到端
├─ 比较迭代次数
├─ 验证数值与工程量
└─ 记录结论或撤销修改8. 观察、解释与下一步
| 观察到什么 | 说明什么 | 下一步检查什么 |
|---|---|---|
| 微基准提升远高于端到端提升 | Amdahl 定律或其他阶段成为新瓶颈 | 阶段占比和优化后的时间线 |
| 单次迭代变快但总时间变慢 | 迭代次数或 Setup 成本上升 | 相同容差下的迭代历史和总成本 |
| CPU Kernel 扩展到某线程数停止 | 带宽、NUMA 或同步达到限制 | 带宽曲线、远端页和线程等待 |
| GPU Kernel 提升但求解器不变 | 传输、主机工作或低调用占比主导 | H2D/D2H、CPU 空洞和调用次数 |
| MPI Rank 增加后成本/步上升 | 已越过经济强扩展点 | 效率、资源成本和周转目标 |
| 性能提升但工程量漂移 | 优化改变了精度、次序或停止路径 | 守恒、残差、工程量和逐场差异 |
9. 最终检查表
- [ ] 优化目标是延迟、吞吐、规模、成本还是能耗?
- [ ] 基线可重复且包含波动吗?
- [ ] 最大热点占比足以产生端到端收益吗?
- [ ] 证据能区分算法问题与实现问题吗?
- [ ] 一次实验是否只有一个主要变量?
- [ ] 是否记录线程、Rank、绑定和硬件?
- [ ] 是否比较迭代次数与每迭代成本?
- [ ] 是否检查守恒、平衡、残差和工程量?
- [ ] 是否保存原始命令、日志和结果?
- [ ] 是否知道收益停止增长的原因?
本专题完成。建议下一步按 [[/06-other/01-projects/profile/C++/2-StructKernelBench/2-StructKernelBench]] 建立可运行的结构算子实验,并把本章模板用于每一次性能改动。
下一篇:[[/06-other/01-projects/profile/C++/2-StructKernelBench/2-StructKernelBench]]