P0跨平台暂存问题解决
This commit is contained in:
1 parent
408b4ecb22
commit
ce62079335
5 files changed
+525
No files matched your search
@@ -1,3 +1,10 @@
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.sh text eol=lf
|
||||
|
||||
# Regression manifests hash these files as raw bytes. Keep their checkout
|
||||
# representation identical on Windows and Linux so hashes remain portable.
|
||||
tests/data/test-mql-8.xml text eol=lf
|
||||
tests/data/test-mql-8.json text eol=lf
|
||||
tests/data/test_mql-full-branches-01-04.xml text eol=lf
|
||||
tests/baselines/simulation/**/*.json text eol=lf
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- "requirements.txt"
|
||||
- "constraints/**"
|
||||
- ".python-version"
|
||||
- ".gitattributes"
|
||||
- "README.md"
|
||||
- ".github/workflows/solver-regression.yml"
|
||||
pull_request:
|
||||
@@ -17,6 +18,7 @@ on:
|
||||
- "requirements.txt"
|
||||
- "constraints/**"
|
||||
- ".python-version"
|
||||
- ".gitattributes"
|
||||
- "README.md"
|
||||
- ".github/workflows/solver-regression.yml"
|
||||
schedule:
|
||||
@@ -60,6 +62,27 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fixture-byte-contract:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'pull_request' ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.suite == 'quick')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-24.04
|
||||
- windows-2022
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: .python-version
|
||||
- name: Verify portable regression fixture bytes
|
||||
run: python -m unittest tests.test_regression_fixture_line_endings
|
||||
|
||||
quick:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
新增或移动文档时,应根据文档用途放入对应目录。目录链接可用于查看其中的全部文档,无需在本文件中逐项维护清单。
|
||||
|
||||
## 重点实施计划
|
||||
|
||||
- [C 语言数值内核实施计划与可行性评估](other/C语言数值内核实施计划与可行性评估.md):后端数值内核的分阶段迁移顺序、验收门、风险和可行性评估。
|
||||
|
||||
## `update-log` 书写规范
|
||||
|
||||
以下规范适用于新建和后续追加的日志。历史日志缺少准确完成时间时,不猜测或补写时间。
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
# C 语言数值内核实施计划与可行性评估
|
||||
|
||||
> 文档状态:待评审实施方案
|
||||
> 建立日期:2026-09-02
|
||||
> 适用分支:`model-development`
|
||||
> 关联文档:[求解器性能优化任务清单](求解器性能优化任务清单.md)、[后端求解逻辑与效率优化调研](后端求解逻辑与效率优化调研.md)
|
||||
> 范围:后端数值执行内核;不包含前端重写,也不主张把整个 Python 后端改写为 C
|
||||
|
||||
## 1. 结论
|
||||
|
||||
这条路线值得实施,但正确的目标不是“把现有 Python 代码逐行翻译成 C”,而是:
|
||||
|
||||
1. 保留 Python 作为模型解析、网络编译、任务管理、结果服务和正确性参考;
|
||||
2. 先把完整的一次 RHS 计算编译成无 Python 回调的扁平数值 IR;
|
||||
3. 再让 C 内核一次完成整次 RHS、stream、物性、Jacobian 和事件计算;
|
||||
4. Python 旧引擎长期保留为不支持模型的兼容路径和故障回退路径。
|
||||
|
||||
总体技术可行性为**中高**。仓库已经具备因果计划、参考 IR、稀疏 Jacobian 结构、性能埋点和 AMESim 回归基础,因此不是从零开始。主要难点不是 C 语法,而是完整冻结当前模型的计算顺序、副作用、事件和回滚语义。
|
||||
|
||||
“实现稳定的 C 数值后端”具有较高可能性;“仅靠换成 C 就达到接近 AMESim 的速度”目前不能承诺。高刚度 LSTP 接触会让积分器产生大量纳秒级小步,有限差分 Jacobian 又会重复调用 RHS。C 能降低每次计算的成本,但不会自动减少这些计算次数。真正的大幅收益需要同时完成:
|
||||
|
||||
- 完整扁平 IR;
|
||||
- 整体 C RHS;
|
||||
- 稀疏解析或半解析 Jacobian;
|
||||
- 正确的接触事件和模式切换;
|
||||
- 必要时再评估原生积分器。
|
||||
|
||||
## 2. 推荐的最终边界
|
||||
|
||||
```text
|
||||
XML / JSON
|
||||
↓
|
||||
Python:解析、校验、组件注册、网络编译
|
||||
↓
|
||||
完整扁平 IR:槽位、阶段、依赖、模式、事件、Jacobian 结构
|
||||
↓ 一次跨语言调用完成整次计算
|
||||
C 数值内核:RHS / stream / 物性 / Jacobian / Event
|
||||
↓
|
||||
SciPy BDF(首阶段保留)或后续通过验证的原生积分器
|
||||
↓
|
||||
Python:采样、结果编码、API、任务状态和诊断
|
||||
```
|
||||
|
||||
以下边界必须坚持:
|
||||
|
||||
- 不在每个组件上来回调用 Python 和 C;一次 RHS 最多进行一次主要跨语言调用。
|
||||
- C 兼容模型的热路径内不得调用 Python callback。
|
||||
- C 内核不直接解析 XML,不管理 HTTP,也不承担组件编辑和数据库职责。
|
||||
- 自定义 Python 组件不能静默降级为跨语言逐组件调用;只要整模型不满足原生能力合同,就明确走 Python 引擎。
|
||||
- 迁移期间始终保留 `python`、`native`、`shadow` 三种模式;通过长时发布门后才能增加 `auto` 默认选择。
|
||||
|
||||
建议统一配置为:
|
||||
|
||||
```text
|
||||
SIMULATION_NUMERIC_ENGINE=python|native|shadow|auto
|
||||
```
|
||||
|
||||
其中 `shadow` 由 Python 控制正式积分,C 只接收相同输入并逐阶段比较,不允许影响 Python 工作区。
|
||||
|
||||
## 3. 当前基础与首个阻断
|
||||
|
||||
### 3.1 已有基础
|
||||
|
||||
- `causal_ir.py` 已有 schema v1、结构签名、预分配工作区、阶段观察和事务回滚原型。
|
||||
- 当前因果执行器已经消去一部分重复逻辑坐标,并证明了数组化和预编译执行计划的方向有效。
|
||||
- 已有 Jacobian 稀疏图、局部切向原语和显式实验回退路径。
|
||||
- 已有 AMESim 结果读取、物理投影、递进时域 runner、性能统计、取消和故障注入测试。
|
||||
- 当前主模型约有 156 个运行组件、20 种组件类型和 132 个连续状态,适合用“能力矩阵 + 分阶段覆盖”推进。
|
||||
|
||||
### 3.2 当前 IR 的缺口
|
||||
|
||||
现有 schema v1 只覆盖全局因果代数计划,绑定仍依赖 Python 的 reader、writer 和 evaluator 回调。以下内容尚未进入同一个无回调执行计划:
|
||||
|
||||
- 状态写入和信号传播;
|
||||
- dynamic volume 和物性状态包;
|
||||
- secondary 压力/流量块;
|
||||
- stream SCC/DAG 和热流体外层闭合;
|
||||
- 状态导数;
|
||||
- 离散 mode、事件检测、reset 和重启;
|
||||
- Jacobian 数值填充;
|
||||
- 结果投影。
|
||||
|
||||
因此,当前 IR 不能直接包一层 C 接口后就获得预期收益。
|
||||
|
||||
### 3.3 P0 跨平台换行阻断(已解决)
|
||||
|
||||
当前 Windows 工作树中的权威输入按原始字节计算时与 manifest 不一致;进一步核对确认,差异全部来自 Git 检出后的 `CRLF` 换行,而不是模型内容变化:
|
||||
|
||||
| 文件 | Windows 工作树原始字节 | 将 `CRLF` 还原为 `LF` 后 | manifest 记录值 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tests/data/test-mql-8.xml` | 106,188 B;2,078 个 `CRLF`;SHA-256 `e7eef641...b801` | 104,110 B;`0a2d9331...7b0b` | 104,110 B;`0a2d9331...7b0b` |
|
||||
| `tests/data/test-mql-8.json` | 282,796 B;10,204 个 `CRLF`;SHA-256 `51e1acb3...11e2` | 272,592 B;`b44bf540...fbe0` | 272,592 B;`b44bf540...fbe0` |
|
||||
| `AmesimModels/test_mql.ame` | 21,708,800 B;`cbc3aadd...c20fbb` | 不适用 | 一致 |
|
||||
|
||||
因此没有证据表明 fixture 语义发生漂移,也不应仅因这一差异重建 golden。真正的问题是:manifest 按原始字节锁定输入,而 `.gitattributes` 没有固定这两个文本 fixture 的换行;当前加载器又直接校验工作树原始字节,所以同一提交在 Windows 上可能被拒绝、在 Linux 上通过。
|
||||
|
||||
该阻断已于 2026-09-02 完成本地修复:`.gitattributes` 已将两个主模型输入、历史 0.81 s 输入和 `tests/baselines/simulation` 下的 JSON 证据固定为 `LF`,当前 Windows 工作树也已从 Git 索引重新检出为 `LF`。自动测试会同时检查属性规则、禁止原始 `CR` 字节,并按 manifest 复核输入大小和 SHA-256;CI 已配置为在 Ubuntu 和 Windows 上分别执行这一合同,远端运行证据待提交后取得。
|
||||
|
||||
本地验收显示这些文件均为 `index=LF / worktree=LF / eol=lf`,两个 regression manifest 可在 Windows 正常完整加载,现有 manifest 和 golden 无需更新。未来只有在规范化后的内容确实变化时,才进入“模型变更、重建 manifest/golden”的流程。历史性能数字仍需按其代码版本看待,但不因换行差异失效。
|
||||
|
||||
## 4. 按重要性排序的实施计划
|
||||
|
||||
优先级定义:
|
||||
|
||||
- `P0`:正确性和架构前置,不完成就不能安全编写生产 C 内核;
|
||||
- `P1`:形成可用且有明显收益的 C 后端;
|
||||
- `P2`:P1 证明有效后再实施的增强项。
|
||||
|
||||
表中的工期是单名熟悉现有求解器的开发者的粗略有效工作量,不是交付承诺,也不包含长时仿真排队时间。
|
||||
|
||||
| 顺序 | ID | 优先级 | 工作项 | 可行性 | 粗略工作量 | 主要依赖 |
|
||||
| ---: | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | C-00 | P0 | 固定权威基准的跨平台合同,建立双引擎与回滚合同 | 高 | 1–2 人周 | 现有 OPT-00 |
|
||||
| 2 | C-01 | P0 | 定义完整数值 IR schema v2 | 高 | 3–5 人周 | C-00、OPT-01/02 |
|
||||
| 3 | C-02 | P0 | 建立纯数值组件 kernel 合同和能力矩阵 | 中高 | 3–6 人周 | C-01 |
|
||||
| 4 | C-03 | P0 | 完成 Python 扁平参考执行器与 Shadow 差分 | 中高 | 3–5 人周 | C-01、C-02 |
|
||||
| 5 | C-04 | P1 | 稳定 C ABI、构建链和隔离 worker | 高 | 3–5 人周 | C-01,可并行 |
|
||||
| 6 | C-05 | P1 | 完成一个闭环代表子系统的 C 纵向切片 | 高 | 2–4 人周 | C-03、C-04 |
|
||||
| 7 | C-06 | P1 | 扩展到完整内置 RHS、stream 和物性 | 中高 | 5–9 人周 | C-05、OPT-04 |
|
||||
| 8 | C-07 | P1 | 稀疏解析/半解析 Jacobian 与局部回退 | 中 | 6–12 人周 | C-02、C-06、OPT-03 |
|
||||
| 9 | C-08 | P1 | LSTP/MECMAS 事件、模式和默认灰度 | 中 | 4–8 人周 | C-06、C-07、OPT-05/06 |
|
||||
| 10 | C-09 | P2 | 模型专用 C 代码生成和编译缓存 | 中 | 4–8 人周 | C-06~C-08 |
|
||||
| 11 | C-10 | P2 | 评估 CVODE;有真实 DAE 需求时再评估 IDA | 中/当前低 | 4–10 人周 | C-06~C-08 |
|
||||
| 12 | C-11 | P2 | 输出、部署、并发和发布收口 | 中高 | 3–6 人周 | 原生默认候选 |
|
||||
|
||||
### 4.1 P0:先固定语义和参考答案
|
||||
|
||||
#### C-00 固定权威基准、双引擎和回滚合同
|
||||
|
||||
工作内容:
|
||||
|
||||
- [x] 固定 `test-mql-8.xml/json`、历史输入和回归证据的跨平台 `LF` 字节合同,实际重新规范化当前 Windows 工作树,并用 Git EOL 状态、原始 SHA-256 和双平台 CI 保护该合同;未重建语义未变的 golden。
|
||||
- 重新加载 manifest 并执行 Python `0.01 s` smoke;只有发现规范化后的内容或物理结果确实变化时,才重新生成结构清单和 golden。
|
||||
- 冻结当前 Python 引擎为参考实现,定义 `python/native/shadow/auto` 的启用条件。
|
||||
- 原生不支持的模型只允许在编译或加载阶段整模型回退;正式验收时禁止静默回退。
|
||||
- 每个阶段使用独立小提交,记录输入哈希、环境、二进制哈希和回滚开关。
|
||||
|
||||
完成标准:相同输入能够稳定强制走 Python;关闭原生后行为与当前参考提交一致;模型、环境和结果来源都能追溯。
|
||||
|
||||
#### C-01 完整数值 IR schema v2
|
||||
|
||||
完整 IR 必须描述共享数值阶段,以及 RHS、Event、Jacobian 和输出四类独立的按需入口。它们可以复用同一套槽位和依赖信息,但不能被误实现成“每次 RHS 都顺序计算 Event、Jacobian 和输出”。共享 primal 计划为:
|
||||
|
||||
```text
|
||||
state scatter
|
||||
→ signal
|
||||
→ mechanical equivalence / dynamic volume
|
||||
→ property bundle
|
||||
→ first pressure-flow closure
|
||||
→ stream SCC/DAG
|
||||
→ temperature reference update
|
||||
→ sensitive-island closure / thermofluid fixed point
|
||||
→ mechanical acceleration
|
||||
```
|
||||
|
||||
四类入口分别编译自己的依赖切片:
|
||||
|
||||
```text
|
||||
eval_rhs = required primal stages → derivative
|
||||
eval_events = required primal stages → event values
|
||||
eval_jacobian = RHS primal / local derivative or JVP → CSR values
|
||||
eval_outputs = required primal stages → output projection
|
||||
```
|
||||
|
||||
IR 至少描述:
|
||||
|
||||
- 连续状态、代数坐标、参数、常量、离散模式和工作区槽位;
|
||||
- 每个阶段和 opcode 的读写集合、单位、缩放、上下界和错误来源;
|
||||
- stream 图、SCC、物性状态包、外层固定点和事务恢复集合;
|
||||
- event ID、左/右状态、reset、Jacobian 失效和模式计划;
|
||||
- 固定的 CSR Jacobian 结构和 output projection;
|
||||
- IR schema、native ABI、组件模型/实现、介质、编译器、dtype、平台和构建选项组成的结构签名。
|
||||
|
||||
原生兼容 program 中不得存在 Python callback。schema v1 保留为参考适配器,不直接扩展成生产 ABI。
|
||||
|
||||
完成标准:同一模型重复编译得到字节级稳定的结构和签名;所有索引及读写集合可静态校验;缺少能力时明确拒绝编译。
|
||||
|
||||
#### C-02 纯数值组件合同
|
||||
|
||||
每个内置组件需声明:
|
||||
|
||||
- `kernel_id` 和实现版本;
|
||||
- 输入、输出、参数、状态和 mode 槽位;
|
||||
- primal、局部导数/JVP、event 和 reset 能力;
|
||||
- 支持的正反流、接触和物性范围;
|
||||
- 类型化错误码和局部数值差分回退边界。
|
||||
|
||||
组件 kernel 必须满足“相同输入得到相同输出”,不能依赖隐藏的 Python 对象写入。旧组件类继续作为参考实现。
|
||||
|
||||
首批纵向切片建议选择一条完整的 `MECMAS21 → PNRP17 → PNCH012 → PNL0001/LSTP00A` 支路及其介质/stream 闭合,因为它同时覆盖机械、气动、物性、流量和高刚度接触。
|
||||
|
||||
#### C-03 Python 扁平参考执行器
|
||||
|
||||
先用 Python/NumPy 完整执行 schema v2,目标是验证语义,而不是追求最终速度。它应摆脱 `PortState` 作为主语义载体,能在每个阶段与当前对象引擎比较首个差异。
|
||||
|
||||
完成标准:代表模型在普通点、反向流点、事件左右和 Jacobian 扰动点逐槽一致;`0.01/0.2/0.81/2.10 s` 的状态、事件、守恒和物理投影满足现有合同。
|
||||
|
||||
### 4.2 P1:形成真正有价值的 C 后端
|
||||
|
||||
#### C-04 稳定 C ABI、构建链和隔离 worker
|
||||
|
||||
底层使用稳定纯 C ABI,Python 绑定保持很薄。最小接口应包含:
|
||||
|
||||
```text
|
||||
model_create
|
||||
eval_rhs
|
||||
eval_jacobian
|
||||
eval_events
|
||||
apply_event
|
||||
eval_outputs
|
||||
model_destroy
|
||||
```
|
||||
|
||||
接口使用不透明 `ModelContext`、固定宽度整数、连续 `float64` 数组和显式长度;CSR 结构在编译时固定,运行时只填 values。每个任务独占可变 context/workspace/cache/mode,不使用可变全局缓存,热路径预分配且不进行常规堆分配。
|
||||
|
||||
首阶段仍由 SciPy BDF 积分,只替换整个 RHS/Jacobian 计算。构建链需覆盖 Windows x64 和 Linux x86_64,固定构建依赖,并禁止 `/fp:fast`、`-ffast-math`、`-march=native` 等会改变事件边界或破坏可移植性的默认选项。
|
||||
|
||||
原生访问违规、崩溃或死循环不能在同一 Python 进程中安全恢复。因此原生默认启用前,仿真必须运行在可硬终止的隔离 worker 中;C 同时接收取消标志并在组件阶段、stream SCC、Jacobian 和迭代处有界检查。
|
||||
|
||||
#### C-05 闭环纵向切片和首次 Go/No-Go
|
||||
|
||||
一次跨语言调用应完成代表子系统的整次 RHS,禁止按组件往返。先为该闭环子系统建立独立代表 fixture,执行 Shadow 和短时积分,不立即扩大组件覆盖。由于此阶段尚未覆盖主模型的全部组件,`test-mql-8` 整模型按能力合同回退 Python 是预期行为,不能拿它衡量 C-05 的端到端收益。
|
||||
|
||||
继续扩面的最低门槛:
|
||||
|
||||
- RHS 微基准至少达到 Python 的 `2×`;目标值为 `3×` 以上;
|
||||
- 独立代表 fixture 的短时完整仿真中位时间至少降低 25%;
|
||||
- 代表 fixture 的状态、事件、模式和守恒门全部通过;存在对应 AMESim 投影时也必须通过;
|
||||
- `nfev/njev/nlu` 和接受步等工作量没有无法解释的变化;
|
||||
- 无 Python callback、无静默回退、无内存错误。
|
||||
|
||||
如果 RHS 已快很多而完整仿真几乎不变,应先检查 Jacobian、微步数和跨语言边界,而不是直接继续扩大 C 代码。
|
||||
|
||||
#### C-06 完整内置 RHS、stream 和物性
|
||||
|
||||
- 把所有受支持的内置组件纳入能力矩阵;一个组件不支持时整模型明确走 Python。
|
||||
- 将 stream 图编译成 SCC 和缩点 DAG:无环部分一次传播,只在循环 SCC 中迭代。
|
||||
- 将 `p/T/rho/u/h` 及其导数组成同一物性状态包,按精确输入和阶段统一复用。
|
||||
- 移除每轮临时字典/列表,使用预分配数组和原地误差统计。
|
||||
- 保留试探状态事务回滚、反向流、温度参考更新和类型化可恢复失败语义。
|
||||
|
||||
完成标准:主目标使用的全部内置组件可原生执行;stream 迭代、压力流量残差和物性结果不恶化;跨平台字节合同修复后的同一权威输入通过短程与中程回归,且 `0.2 s` 完整仿真中位时间至少降低 30%。
|
||||
|
||||
#### C-07 稀疏解析/半解析 Jacobian
|
||||
|
||||
这是获得大幅端到端收益的关键步骤。当前历史报告中,有限差分 Jacobian 会贡献大量额外 RHS 调用;仅把 primal RHS 改成 C 仍会重复执行它。
|
||||
|
||||
- 组件局部导数沿完整 IR 传播,直接填充固定 CSR values。
|
||||
- 不支持或非光滑点只对局部组件、状态列或 SCC 做数值差分,不能让一个局部问题恢复全网有限差分。
|
||||
- 每次构建记录解析列、局部差分列、回退位置、JVP 审计和装配时间。
|
||||
- LSTP 接触、流向切换、饱和及临界流动使用分段导数,并在边界上明确选择事件分段或局部回退。
|
||||
|
||||
完成标准:有限差分附加 RHS 至少减少 50%,相对完整 C RHS 阶段再降低至少 20% 总时间;事件顺序、模式和物理投影不变。
|
||||
|
||||
#### C-08 事件、模式和默认灰度
|
||||
|
||||
- 将 LSTP/MECMAS 的 event、mode、reset、Jacobian 失效和 solver restart 纳入正式 IR/C 合同。
|
||||
- 对接触进入、保持、释放,上下端挡、回弹、同时事件、擦边事件和防抖分别测试。
|
||||
- 按 `shadow → 显式 native → 模型签名白名单 → auto` 逐级启用。
|
||||
- 只有当前权威 `10 s` 连续三次通过后,才允许受支持模型默认走 C。
|
||||
|
||||
需要特别注意:C-08 不只是把同一接触公式写成 C,还要减少因接触表达方式造成的不必要纳秒级微步。任何软化、容差或事件改动都属于数值算法变化,必须与纯执行优化分开提交和验收。
|
||||
|
||||
### 4.3 P2:证明 P1 有效后再做
|
||||
|
||||
#### C-09 模型专用 C 代码生成
|
||||
|
||||
通用 C IR 解释器稳定后,若 opcode 分派仍是明确热点,再为固定模型生成专用 C。XML 文本不得直接拼入源码;生成器只消费校验后的数值 IR。编译缓存键必须包含完整结构签名、编译器和 flags。
|
||||
|
||||
只有相对通用 C 执行器额外获得至少约 `1.5×` 的稳定收益,并且编译成本能在重复运行中摊销时才继续。否则保留通用执行器,放弃这一层复杂度。
|
||||
|
||||
#### C-10 原生积分器
|
||||
|
||||
- 当前系统是“RHS 内完成代数闭合”的半显式 ODE,先评估 CVODE,不直接切换为 DASSL/IDA。
|
||||
- 只有 C RHS、Jacobian、事件、可恢复缩步、partial result、activity 和 cancel 合同稳定后,才对 CVODE 做独立 A/B。
|
||||
- CVODE 在相同正确性门下额外降低至少 25% 总时间才值得替换 SciPy。
|
||||
- IDA/DASSL 需要新的 `F(t,y,ydot)=0`、代数变量布局、一致初始化、指数和质量矩阵合同。只有真实模型证明无法稳健因果化或 ODE 化时再立项,当前不把它视为提速开关。
|
||||
|
||||
#### C-11 输出、部署和发布收口
|
||||
|
||||
吸收按需结果变量、列式输出、结果分块、编译缓存、并发限流、worker 硬终止和原生崩溃恢复。确保 native 崩溃只影响当前任务,服务仍能接收新任务。
|
||||
|
||||
## 5. 分阶段验证与 Go/No-Go 门
|
||||
|
||||
| 验证阶段 | 主要内容 | 通过条件 | 未通过时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| V0 | fixture、环境、Python 和 AMESim 基线 | 来源一致;Python 连续 3 次稳定;现有测试通过 | 基线仍漂移时停止 C 默认路径开发 |
|
||||
| V1 | 完整 Python IR | 槽位、阶段、读写集合、事件和回滚差分通过 | 先修隐藏副作用和 IR 合同 |
|
||||
| V2 | C 纵向切片 | 单组件/逐阶段通过;RHS 至少 `2×` | 未达门槛则检查 IR/FFI,必要时停止扩面 |
|
||||
| V3 | 完整 C RHS + SciPy | `0.01/0.2 s` 通过;总时间至少降低 30%;工作量变化不超过可解释范围 | 不作为纯执行优化合入默认路径 |
|
||||
| V4 | C Jacobian/Event | FD 附加 RHS 至少减少 50%;相对 V3 再降低 20%;事件/模式一致 | 保留局部差分,禁止整网静默回退 |
|
||||
| V5 | 长时与鲁棒性 | `1/5/10 s` 递进;10 s 连续 3 次;无泄漏、无静默回退 | 任一短时前驱失败即停止后续长跑 |
|
||||
| V6 | 默认启用 | Windows/Linux 发布通过;支持模型走 C,不支持模型明确走 Python;可一键关闭 | 保持显式 opt-in |
|
||||
|
||||
正式性能比较统一要求:预热 1 次、完整仿真至少测量 3 次并比较中位数、固定机器与电源模式,冷编译和热缓存分开报告。至少记录:
|
||||
|
||||
- 总时间、积分、RHS、闭合、stream、物性、Jacobian 和输出时间;
|
||||
- `nfev/njev/nlu`、接受/拒绝步、solver 启动和事件次数;
|
||||
- Jacobian 附加 RHS、局部数值差分和回退原因;
|
||||
- Python/C 边界调用次数和耗时;
|
||||
- 峰值 RSS、C 工作区、IR/ABI 版本、编译器、flags 和二进制哈希。
|
||||
|
||||
附加硬门:关闭 C 时 Python 路径额外开销不超过 2%;纯执行层替换若使 `nfev/njev/nlu` 或接受步数变化超过 2%,必须按数值语义变化单独调查,不能直接归为性能优化;峰值 RSS 不超过 Python 基线的 110%;10 s 中 C 固定工作区不随步数增长,只有结果数组可随采样数增长。
|
||||
|
||||
## 6. 正确性验证范围
|
||||
|
||||
### 6.1 Shadow 阶段观察点
|
||||
|
||||
两个引擎必须接收完全相同的 `time/state/mode/parameters`,按第 4.1 节的 RHS 阶段逐项比较。差异报告至少包含:阶段、槽位、组件、输入、Python 值、C 值、绝对/相对误差和当前模式。
|
||||
|
||||
- 槽位布局、模式码、事件 ID、执行顺序和迭代次数要求一致。
|
||||
- 连续量使用“每种物理量绝对容差 + 相对容差”,不能用一个绝对容差覆盖压力、流量、温度和位移。
|
||||
- NaN/Inf、压力越界、符号错误和模式不同立即失败。
|
||||
- 压力流量最大缩放残差继续使用现有 `1e-7` 门。
|
||||
- 事件点分别比较左极限、事件处理结果和右极限,不扩大容差掩盖不连续。
|
||||
|
||||
差分语料至少覆盖:初始状态、接受状态、Jacobian 扰动状态、固定随机种子扰动、压力近似相等、流量换向、接触启停、端挡释放、物性边界,以及历史 `0.69/0.81/2.05 s` 区域。
|
||||
|
||||
### 6.2 AMESim 物理门
|
||||
|
||||
Python golden 只用于发现实现漂移,AMESim 结果仍是外部物理基线。正式默认前,应在现有 physical-state-v2.1 基础上至少覆盖:
|
||||
|
||||
- 代表性气室压力、温度和质量;
|
||||
- PNL 管路压力、流量及换向;
|
||||
- PNRP 活塞力;
|
||||
- MECMAS 位移、速度和模式;
|
||||
- LSTP 间隙、接触力和接触切换;
|
||||
- 每个主要支路至少一个代表量。
|
||||
|
||||
检查点至少包含 `0`、`0.04 s` 左右、`0.2`、`0.8 s` 左右、`1`、`2`、`5` 和 `10 s`。AMESim 未保存的内部守恒量继续执行独立绝对残差门。
|
||||
|
||||
### 6.3 错误、取消和并发
|
||||
|
||||
必须覆盖 C 返回非有限值、物性域错误、闭合不收敛、未知 opcode、ABI 不匹配、损坏二进制、失败后事务回滚、编译失败、并发 context 隔离、单任务取消、重复创建/销毁无内存增长和 native 崩溃后的服务存活。
|
||||
|
||||
回退分为三层:
|
||||
|
||||
1. **启动前自动选择**:编译或加载时发现不支持能力,整次任务在开始积分前明确选择 Python;这是正式模式唯一允许的自动换引擎路径。
|
||||
2. **受控运行时错误**:native 已开始积分后返回错误时,当前 native 任务直接失败并保留诊断。灰度验证可以从不可变原始请求的 `tStart` 另起一个 Python 重放任务,但必须标为独立重放,不能算作 native 成功。没有完整保存 BDF 历史、事件上下文和已有输出前,禁止从某个 `(t,y,mode)` 假装无缝续跑。
|
||||
3. **进程级故障**:访问违规或无法返回时,由父进程硬终止 worker,禁止在受损进程中继续;如需 Python 对照,同样从原始请求重新执行。
|
||||
|
||||
正式 C 验收要求异常 `fallbackCount=0`。声明过的局部 Jacobian 数值差分不是异常回退,但必须单独计数。
|
||||
|
||||
## 7. 对现有优化清单的取舍
|
||||
|
||||
现有优化清单不能整体放弃,应按新路线重组:
|
||||
|
||||
| 现有任务 | 决策 | 在新路线中的位置 |
|
||||
| --- | --- | --- |
|
||||
| OPT-00 基线与回归 | 保留并加强 | C-00 和全部发布门 |
|
||||
| OPT-01 因果代数内核 | 冻结成果 | 作为 IR 编译输入和 Python 回退,不再继续零散微调 |
|
||||
| OPT-02 扁平 IR | 升为主线 | C-01、C-03、C-05 |
|
||||
| OPT-03 Jacobian | 升为主线 | C-07 |
|
||||
| OPT-04 stream/物性 | 升为主线 | C-06 |
|
||||
| OPT-05 步长、接触和鲁棒性 | 必须保留 | C-08;C 不能消除微步根因 |
|
||||
| OPT-06 dense output | 部分前置、其余后置 | 事件合同进入 C-01/C-08,插值微调后置 |
|
||||
| OPT-07 输出与内存 | 暂缓但不删除 | C-11 |
|
||||
| OPT-08 取消与并发 | 必须保留并提前 | C-04/C-11;原生代码更需要进程隔离 |
|
||||
| OPT-09 10 s 验收 | 必须保留 | V5/V6 发布门 |
|
||||
| OPT-10 高指数 DAE | 有条件暂缓 | 只有真实 DAE 需求时进入 C-10 |
|
||||
|
||||
可以停止继续投入的方向:
|
||||
|
||||
- 在现有对象热路径上继续做零散字典和属性访问微优化;
|
||||
- 继续叠加缺少统一失效合同的通用缓存;
|
||||
- 优化目标模型中未触发的 `least_squares` 回退;
|
||||
- 不断增加少量特例半解析列,而不先建立通用组件导数合同;
|
||||
- 直接对当前对象图使用 Numba;
|
||||
- 通过修改容差、软化物理或寻找“幸运 maxStep”伪装性能收益。
|
||||
|
||||
Numba 可以在完整数组 IR 后用于 1–2 周的架构验证,但不作为长期生产依赖。若完整数组 RHS 在 Numba 原型中仍没有明显改善,应先修正 IR 和算法,而不是立即开始大规模 C 重写。
|
||||
|
||||
## 8. 可行性评估
|
||||
|
||||
| 目标 | 当前判断 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 验证框架、双引擎和基准恢复 | 高 | 现有 runner、golden、AMESim 读取和埋点可复用 |
|
||||
| 完整 Python 扁平 IR | 中高 | 结构基础已有,主要工作是显式化隐藏副作用和事件语义 |
|
||||
| 代表子系统通用 C RHS | 高 | 数值边界清楚,一次调用可避免 FFI 碎片化 |
|
||||
| 当前全部内置组件的 C RHS | 中高 | 约 20 种类型可逐类迁移,但 stream/物性/模式较复杂 |
|
||||
| 可维护的稀疏解析/半解析 Jacobian | 中 | 收益大,但非光滑接触、流向切换和导数覆盖难度最高 |
|
||||
| 模型专用 C 代码生成 | 中 | 收益上限高,但构建、缓存、安全和诊断成本明显增加 |
|
||||
| CVODE 替换 SciPy | 中,且后置 | 可能减少调度开销,但不会自动解决错误方程或接触微步 |
|
||||
| IDA/DASSL 直接提速 | 当前低 | 目前缺少真正 DAE 的残差、一致初始化和质量矩阵合同 |
|
||||
| 接近 AMESim 的总速度 | 中低、待实测 | 缺少同机 AMESim 墙钟基线,且当前主要瓶颈同时包含 Jacobian 和接触微步数 |
|
||||
|
||||
从项目节奏看,建议把目标分成三档;这里与第 4 节一样使用“人周”,多人并行时日历时间可短于人周总量:
|
||||
|
||||
1. **约 15–27 人周:技术决策闭环。** 完成 C-00~C-05,回答完整 IR 是否正确、C RHS 是否有足够收益。
|
||||
2. **累计约 20–36 人周:可选原生后端。** 完成 C-06,使主要内置 RHS、stream/物性、双平台构建和受控回退可由用户显式启用。
|
||||
3. **累计约 30–56 人周:默认候选。** 完成 C-07/C-08、10 s 长时、并发隔离和发布门。
|
||||
|
||||
模型专用代码生成和原生积分器属于额外阶段,不应计入首个可用 C 后端的承诺。多人并行可以缩短日历时间,但 IR、组件合同、Jacobian 和事件语义存在强依赖,不能按人数等比例压缩。
|
||||
|
||||
## 9. 建议立即开展的第一批工作
|
||||
|
||||
第一批只做 P0,不直接开始大规模 C 编码:
|
||||
|
||||
1. **已完成:** 固定当前权威输入和回归证据的 `LF` 检出规则,重新规范化 Windows 工作树,并增加 Windows/Linux 字节合同测试;未重建语义未变的 golden。
|
||||
2. 生成当前模型的组件类型、槽位、阶段、副作用和事件能力矩阵。
|
||||
3. 将 schema v2 写成独立规范,先冻结 RHS 阶段、错误、事务、事件和结构签名。
|
||||
4. 建立对象引擎与扁平 IR 的逐阶段 Shadow runner。
|
||||
5. 用一条完整机械—气动—管路—接触支路完成 Python 参考闭环。
|
||||
6. 评审通过后,再建立最小 C ABI 和纵向切片。
|
||||
|
||||
第一批的退出条件不是“已经写了多少 C”,而是:当前基准可信、同一计算能被无回调 IR 完整表达、差异能定位到具体阶段和槽位。只有达到这个条件,后续 C 工作才具有可预测的收益和可控的回滚成本。
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
BASELINE_ROOT = REPOSITORY_ROOT / "tests" / "baselines" / "simulation"
|
||||
GIT_ATTRIBUTES_PATH = REPOSITORY_ROOT / ".gitattributes"
|
||||
|
||||
EXPECTED_ATTRIBUTE_RULES = {
|
||||
("tests/data/test-mql-8.xml", "text", "eol=lf"),
|
||||
("tests/data/test-mql-8.json", "text", "eol=lf"),
|
||||
("tests/data/test_mql-full-branches-01-04.xml", "text", "eol=lf"),
|
||||
("tests/baselines/simulation/**/*.json", "text", "eol=lf"),
|
||||
}
|
||||
|
||||
|
||||
def _manifest_paths() -> tuple[Path, ...]:
|
||||
return tuple(sorted(BASELINE_ROOT.glob("*/manifest.json")))
|
||||
|
||||
|
||||
def _manifest_source_descriptors() -> tuple[tuple[Path, dict[str, object]], ...]:
|
||||
descriptors: list[tuple[Path, dict[str, object]]] = []
|
||||
for manifest_path in _manifest_paths():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
source = manifest["source"]
|
||||
descriptors.append((manifest_path, source))
|
||||
companion = source.get("companionProject")
|
||||
if companion is not None:
|
||||
descriptors.append((manifest_path, companion))
|
||||
return tuple(descriptors)
|
||||
|
||||
|
||||
class RegressionFixtureLineEndingTests(unittest.TestCase):
|
||||
def test_gitattributes_pin_byte_locked_artifacts_to_lf(self) -> None:
|
||||
attribute_rules = {
|
||||
tuple(line.split())
|
||||
for raw_line in GIT_ATTRIBUTES_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if (line := raw_line.strip()) and not line.startswith("#")
|
||||
}
|
||||
|
||||
self.assertTrue(
|
||||
EXPECTED_ATTRIBUTE_RULES.issubset(attribute_rules),
|
||||
"Raw-byte regression artifacts must be explicitly pinned to LF.",
|
||||
)
|
||||
|
||||
def test_byte_locked_artifacts_contain_no_carriage_returns(self) -> None:
|
||||
paths = {
|
||||
path
|
||||
for path in BASELINE_ROOT.rglob("*.json")
|
||||
if path.is_file()
|
||||
}
|
||||
paths.update(
|
||||
REPOSITORY_ROOT / str(descriptor["path"])
|
||||
for _, descriptor in _manifest_source_descriptors()
|
||||
)
|
||||
|
||||
self.assertTrue(paths, "No byte-locked regression artifacts were found.")
|
||||
for path in sorted(paths):
|
||||
with self.subTest(path=path.relative_to(REPOSITORY_ROOT).as_posix()):
|
||||
self.assertNotIn(
|
||||
b"\r",
|
||||
path.read_bytes(),
|
||||
"Byte-locked regression artifacts must use LF line endings.",
|
||||
)
|
||||
|
||||
def test_manifest_source_identities_match_raw_worktree_bytes(self) -> None:
|
||||
self.assertTrue(_manifest_paths(), "No regression manifests were found.")
|
||||
for manifest_path, descriptor in _manifest_source_descriptors():
|
||||
relative_path = str(descriptor["path"])
|
||||
payload = (REPOSITORY_ROOT / relative_path).read_bytes()
|
||||
label = f"{manifest_path.parent.name}:{relative_path}"
|
||||
with self.subTest(source=label):
|
||||
self.assertEqual(len(payload), descriptor["bytes"])
|
||||
self.assertEqual(
|
||||
hashlib.sha256(payload).hexdigest(),
|
||||
descriptor["sha256"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user