生产级求解器架构:模块边界、执行管线与可观测性 / Production Solver Architecture with Module Boundaries, Pipelines, and Observability
📅 创建时间:2026-07-23
🏷️ 标签:#求解器架构 #数据模型 #线性代数后端 #可观测性
📚 前置知识:[[00-solver-engineering-overview]] [[../01-foundations-and-architecture/09-architecture]]
📚 相关知识:[[../marine-cae/02-cae-workflow]] [[/01-cpp/10-plugin-engineering/07-plugin-interface-design]]
1. 从算法原型到工业软件
教学程序常把所有步骤写在一个函数里:
读网格 → 组装矩阵 → 调求解器 → 写结果工业软件需要长期支持不同单元、物理模型、硬件后端、文件格式和版本,因此必须把“变化原因不同”的部分分开:
- 网格拓扑不应依赖具体线性求解器;
- 物理模型不应直接调用某个厂商的稀疏矩阵 API;
- 非线性收敛控制不应散落在材料和单元代码中;
- 输出模块不应决定计算过程中的数据布局;
- MPI、GPU 等执行细节应通过清晰边界进入,而不是形成全局条件分支。
2. 端到端执行管线
1. 解析输入和版本迁移
2. 单位、拓扑、材料和边界条件检查
3. 建立网格、区域、场变量和自由度
4. 构造邻接关系与稀疏模式
5. 初始化物理模型、求解器和并行环境
6. 进入载荷步、时间步或耦合步
7. 组装矩阵/右端项,或计算残差/算子作用
8. 求解线性或特征值子问题
9. 更新状态并检查非线性、时间和物理收敛
10. 输出监控量、结果和 Checkpoint
11. 完成统计、释放资源并生成运行摘要这个流程应由高层 Driver 控制。底层 Kernel 只完成局部计算,不决定什么时候结束整个求解。
3. 核心数据模型
3.1 Mesh
负责节点、单元、Cell、Face、拓扑连接、边界区域、材料区域、分区、局部到全局编号以及 Ghost/Halo 实体。面积、体积、法向和 Jacobian 等几何量可以按生命周期缓存。
Mesh 不应该存储所有物理场的业务含义,否则每增加一个模型都要修改网格核心。
3.2 Field
Field 描述“某个量在哪里、多少分量、什么精度、由谁拥有”:
Field
├─ Location: Node / Element / Cell / Face / IntegrationPoint
├─ Components: 1 / 3 / 6 / ...
├─ ScalarType: float / double
├─ MemorySpace: Host / Device
├─ Ownership: Owned / Ghost
└─ Layout: AoS / SoA / AoSoA温度、位移、速度、压力、应力和湍流变量可以使用同一套 Field 抽象,但具有不同语义和更新规则。
3.3 DofManager
结构、热、电磁等有限元问题需要 DofManager:
- 注册节点或单元上的自由度;
- 生成局部到全局方程编号;
- 处理固定、耦合和多点约束;
- 提供单元自由度列表;
- 为稀疏模式构造邻接;
- 在 MPI 中维护本地与全局编号映射。
编号策略会影响矩阵带宽、Fill-in、Cache 局部性和分区通信,因此它不是纯粹的“编号工具”。
4. 计算组件边界
4.1 PhysicsModel
定义材料本构、湍流模型、源项和边界通量。它应输入明确的局部状态,输出局部残差、切线矩阵或通量,不直接管理全局求解循环。
4.2 Operator
Operator 表示代数作用:
y = A(x)它可以由显式稀疏矩阵实现,也可以是 Matrix-Free 算子。Krylov 求解器只依赖 Operator 接口,因而不必知道矩阵是否真正存储。
4.3 Assembler
Assembler 负责把局部贡献放入全局对象:
- 单元矩阵和载荷组装;
- CFD 面通量对 owner/neighbour 的贡献;
- 边界条件修改;
- 原子操作、图着色或线程局部缓冲;
- MPI 本地贡献与 Ghost 更新。
局部物理计算和全局写入策略应分开,便于在串行、OpenMP 和 GPU 后端之间切换。
4.4 LinearSolver 与 Preconditioner
典型接口需要区分不同生命周期:
analyzePattern(A) 稀疏结构分析,可跨多个载荷步复用
factorize/setup(A) 数值分解或预条件建立
solve(A, x, b) 求解
update(A) 系数变化后的增量更新
statistics() 迭代、残差、时间和内存直接法尤其需要区分符号分解和数值分解;多右端项场景中复用因子可以带来巨大收益。
4.5 NonlinearDriver
负责载荷增量、Newton 或 Picard 外层迭代、状态提交与回滚、收敛准则、线搜索、阻尼和失败重试。
材料模型只提供试算状态与切线。是否提交塑性历史变量由 Driver 决定,否则失败迭代可能污染状态。
4.6 TimeIntegrator
时间积分器负责时间离散系数和历史状态,例如 Newmark、Generalized-α、BDF 和 Runge-Kutta。它不应直接实现具体物理残差,而是向物理模型提供当前时间、阶段和历史组合。
5. 高层伪代码
Model model = ModelReader::load(config.input);
ValidationReport report = validate(model, config);
report.requireNoFatalErrors();
Runtime runtime(config.parallel);
SolverContext ctx(model, runtime);
ctx.initializeFields();
ctx.buildTopology();
ctx.buildAlgebra();
for (Step step : ctx.stepController()) {
ctx.beginStep(step);
while (!ctx.convergence().finished()) {
ctx.restoreTrialState();
ctx.assembler().evaluate(ctx.state(), ctx.system());
ctx.boundaries().apply(ctx.system());
SolveReport linear = ctx.linearSolver().solve(
ctx.system().op, ctx.increment(), ctx.system().rhs);
ctx.updateState(linear);
ctx.convergence().observe(ctx.metrics());
}
ctx.commitStep();
ctx.resultWriter().writeIfScheduled(ctx);
ctx.checkpoint().saveIfScheduled(ctx);
}这里最重要的不是类名,而是状态生命周期、求解控制和局部计算之间存在明确边界。
6. 线性代数后端适配
工业求解器可能支持自研 CSR/BSR、Eigen、oneMKL、PETSc、Trilinos、Hypre、cuSPARSE、cuSOLVER、MUMPS 或 PARDISO。
推荐分层:
Physics / Discretization
↓
项目内部 Vector / Operator / Solver 接口
↓
Backend Adapter
↓
PETSc / Hypre / MKL / CUDA / 自研实现适配层统一生命周期、错误码、精度、索引宽度和统计信息。不要把 PETSc 对象或 CUDA 指针扩散到所有业务模块,否则后端替换和单元测试都会很困难。
7. 插件和注册机制
适合插件化的变化点包括单元类型、积分规则、材料本构、边界条件、湍流与多相模型、线性求解器、预条件器、派生结果和输出格式。
注册信息至少应包括:
- 稳定名称和版本;
- 支持的空间维度与分析类型;
- 所需输入字段和生成字段;
- 参数 Schema、默认值和单位;
- CPU/GPU、单精度/双精度等能力约束;
- 序列化和版本迁移规则。
加载时先验证兼容性,避免求解中途才发现字段缺失。
8. 状态、失败和恢复
8.1 状态分层
Committed State 上一个成功步,允许恢复
Trial State 当前非线性迭代的试算状态
Scratch State Kernel 临时量,不进入 Checkpoint接触、塑性、损伤和湍流模型都可能有历史状态。必须明确何时复制、何时提交、何时回滚。
8.2 失败分类
| 类型 | 例子 | 合理处理 |
|---|---|---|
| 输入错误 | 单位缺失、负材料参数 | 求解前停止并定位字段 |
| 数值失败 | 线性不收敛、矩阵奇异 | 输出矩阵和收敛证据,尝试受控降步 |
| 资源失败 | 内存不足、设备丢失 | 保存可恢复状态,报告资源需求 |
| 通信失败 | Rank 异常、文件系统超时 | 一致终止或从 Checkpoint 重启 |
| 软件缺陷 | NaN 来源不明、越界 | 保留最小复现和诊断快照 |
不能用无限重试掩盖输入或软件错误。失败处理必须确保所有 MPI Rank 对下一步动作达成一致。
9. Checkpoint 与可重复性
Checkpoint 应记录:
- 网格和分区标识;
- 当前步、时间和非线性迭代状态;
- 所有必须的历史场;
- 求解器配置、容差和版本;
- 随机种子以及必要的并行元数据。
并行浮点归约次序不同可能产生微小差异。对强非线性或混沌问题,应定义“允许的工程等价”,而不是要求每一位二进制完全相同。回归测试同时比较残差历史、关键工程量和守恒误差。
10. 可观测性
10.1 阶段级
- 网格读取、初始化、组装、线性求解、后处理、I/O;
- 每个载荷步或时间步的耗时;
- 峰值内存和显存。
10.2 算法级
- 外层与线性迭代次数;
- 残差和增量历史;
- 预条件建立和应用时间;
- 接触对、非正交修正次数等模型指标。
10.3 并行级
- 各线程或 Rank 的工作量;
- Halo、集合通信和 Barrier 时间;
- GPU Kernel、拷贝和同步时间;
- 各 Rank 输出量与 I/O 等待。
所有统计应能关联到同一个 Run ID、输入摘要和配置快照。高频 Kernel 中不要逐实体输出日志,应使用计数器、采样或聚合统计。
11. 配置快照与可追溯性
一次可复现实验至少保存:
输入模型哈希
求解器版本和 Git 提交
编译器与优化选项
依赖库、驱动和 MPI 版本
硬件拓扑
线程、Rank 和设备绑定
求解容差与停止准则
输出与 Checkpoint 策略只保存图形界面截图不能复现实验。GUI 最终应生成稳定、可比较的配置表示。
12. 架构审查清单
- [ ] 网格拓扑是否独立于具体物理模型?
- [ ] 物理 Kernel 是否能在不启动完整求解器时测试?
- [ ] 非线性 Driver 是否统一管理试算、提交和回滚?
- [ ] 线性代数后端是否通过适配层隔离?
- [ ] 是否区分符号结构、数值系数和右端项的生命周期?
- [ ] CPU/GPU 之间是否存在隐式数据拷贝?
- [ ] 并行归约和错误处理是否保持所有 Rank 一致?
- [ ] Checkpoint 是否足以从失败点继续?
- [ ] 日志是否能解释一次求解为什么慢或为什么不收敛?
- [ ] 性能计时是否默认关闭昂贵的逐实体日志?
13. 架构诊断三问
| 观察到什么 | 说明什么 | 下一步检查什么 |
|---|---|---|
| 更换线性代数后端需要修改物理模块 | 后端类型已经泄漏到业务层 | Operator、Vector 和 Adapter 边界 |
| 失败增量后材料状态异常 | Trial/Committed 生命周期混乱 | 状态提交、回滚和深浅复制 |
| Restart 后结果与连续运行差异大 | Checkpoint 信息不完整或恢复顺序错误 | 历史场、时间积分状态和配置快照 |
| 无法解释某一步突然变慢 | 可观测性粒度不足 | Step、方程、Rank 和 Kernel 统计 |
| GPU 版本存在大量隐式拷贝 | Field 所有权与 MemorySpace 不清晰 | Host/Device 权威副本和同步点 |
14. 核心结论
生产级求解器的核心不是“调用一个线性求解库”,而是管理数据、状态、算法和硬件的生命周期。好的边界使物理模型、数值算法和执行后端能够独立演进;坏的边界会把一次局部优化变成全系统重写。
下一篇:[[02-structural-solver-design]]