教训¶
这个代码库里的每条规则都是付过钱的。本页是收据抽屉:守卫、钉死、以及你 维护桥时会遇到的响亮失败背后的那些事故。每条都按同一个形状写——症状、 根因、修复、留下的规则。涉事的机器与账户刻意匿名;能迁移的是机制。
1. 任何比较的两边都要钉死 Julia 运行时¶
症状。 四道 GRI 验收门(验收门)开始报出 1e-9 量级的非零余量——时过时不过——而桥和化学都没改过。看得见的嫌疑人 是桥的数组提取。
根因。 版本错位,不是 bug。桥可执行文件内嵌构建钉死的 libjulia
1.12.7,而原生基准是用裸 julia 重新生成的——那是一个自动更新的版本
shim,上游一发版就悄然指向 1.13.2。同一个刚性求解、两个不同 Julia 版本
各跑一遍,点火过渡段就会漂移——实测逐点达 T rel 1.5e-5、CH4 rel
0.54——轻松跨过 1e-8 的门。调查先追了求解器非确定性与 BLAS 线程归约顺
序;用钉死二进制重新生成基准,四个偏差精确归零,结案。
修复。 现在有两道守卫响亮拒绝错位:run_gri_native.jl 除非
VERSION == v"1.12.7" 否则报错;FJ_BUILD_SYSIMAGE 的 CMake 守卫在配置
期拒绝一切 1.12.x 线以外的构建 Julia,并点名版本 shim 是不能用的东西。
规则。 任何 A/B 比较——基准 vs 桥、改前 vs 改后——两边用同一个钉死 二进制。紧门 flake 时,先审计运行时,再查代码。
2. 用 [compat] 钉死 SciML 栈¶
症状。 capstone 的每格化学开销突然暴涨约 700 倍:冻结网格格走 982 步积分器内步而不是 1 步,燃烧格约 5.4 万步而不是约 100 步,每格分配约 3.5 MB。nx = 400 时化学阶段跑 89 s,而受测栈只需 1.25 s。
根因。 草稿环境漂移到了 OrdinaryDiffEq 7.x 线上,因为动力学包当时不
带 [compat] 边界。7.x 的步长控制器处理每格 reinit! / step! 模式要
差得多——一个”解析成功”的依赖,跑起来很糟。
修复。 包的 [compat] 现在钉死受测线——Catalyst ~16.2、
OrdinaryDiffEq ~6.111、SciMLBase ~2.155、StaticArrays ~1.9、
PrecompileTools ~1.3——删掉未用的 ModelingToolkit 直接依赖,环境装配脚本
写明这些钉死为何载重。
规则。 数值依赖一律钉死;吞吐无缘无故退化时,先打印解析出的版本再 profile。”能解析”不等于”能干活”;resolver 优化的是兼容性,不是你的访问 模式。
3. 永远不要关掉 Julia 的信号处理¶
症状。 接启动选项时,把 jl_options.handle_signals = OFF 看成让嵌
入运行时不抢宿主应用信号处理器的干净办法。进程随后在 GC safepoint 崩
溃——15 次运行里 14 次。
根因。 嵌入式 Julia 的 GC safepoint 轮询依赖 Julia 自己的 SIGSEGV 处 理器;关掉信号处理等于拆掉运行时自己要用的机器。崩溃是必现的、不是竞态 ——至少它好复现。
修复。 桥永不写 jl_options.handle_signals。它本要满足的需求(”别
接管我的信号”)换了个满足法:fcj_options_t 里的 handle_signals = 0
含义是不接管——init 成功后桥把 SIGINT / SIGTERM 重置为 SIG_DFL,
GC 关键的处理器原封不动。
规则。 把 jl_options 当只读。需要宿主友好的信号处置,就在 init 后
重置具体信号;永远不要从 Julia 脚下抽走它自己的处理器。
4. 共享集群上:以退出码判作业,别看 stdout¶
症状。 在共享集群上经 Slurm 的 srun 启动的批作业会间歇性地把整段
stdout 打印两遍——整块逐字重复、跨独立运行复现、作业本身一切健康。裸的
shell echo 也能复现;没有环境变量或节点设置解释得了它。
根因。 启动层的集群 I/O 怪象,不是被跑程序的错。它是传输层噪声,作 业内部修不了。
修复与规则。 自动化判定一律以退出码和测试汇总行为准,绝不以日志
读起来像什么为准;日志归档或续传时先去重(awk '!seen[$0]++' 是本仓库
的惯用语)。解析 Slurm 日志找哨兵串并据此行动时,永远为重复留出余量——
也别让一份看着重复的日志说服你作业跑了两遍。
5. 一个首次运行就下载 570 MB 的”自含”AOT 库¶
症状。 一座 PackageCompiler 构建、宣称自含的库,第一次初始化花在网 络抓取上——约 570 MB(IntelOpenMP / oneTBB / MKL)——发生在一台没有包 depot 的机器上。
根因。 SciML 闭包里的一个依赖在镜像加载时探测 MKL_jll——一个
惰性 artifact。PackageCompiler 默认只捆绑非惰性 artifact,于是”你要的
一切”悄悄变成了”除惰性以外的一切”。
修复。 构建带 include_lazy_artifacts = true;capstone 的 AOT 构建
脚本原样带着解释此事的注释。验证方式是老实的那种:在空
JULIA_DEPOT_PATH 和全新 home 目录下初始化——零网络访问。
规则。 “自含”是可检验的命题——用空 depot 和断网路径检验它,而不是 读自己的构建脚本。
6. AOT 库拥有自己的运行时:走它自己的正门¶
症状。 把编译出的 Julia 库链进一个同时用桥 SDK 的进程——能链上—— 直到初始化段错误,或者以指向四面八方的方式行为异常。
根因。 编译出的库捆绑自己的 libjulia,而进程现在对运行时住在哪有
了两套意见。调 SDK 的 julia_init() 走的是裸 jl_init(),它会找捆绑布
局里根本没有的默认 sys.so——sysimage 是编译进库里的,只有它导出的
init_julia 知道怎么加载它(带显式 bindir)。
修复。 AOT 模式下,生命周期只走库导出的 init_julia /
shutdown_julia,别无他途。接口声明在
src/lib_support/julia_library_interface.f90,工作示例
(examples/03_deployment/2_compiled_library/、
examples/07_capstone/3_aot_kinetics/)展示了这个模式。
规则。 一个进程、一个运行时所有者。库自带 Julia 时,生命周期入口是 那个库的导出函数——不是 SDK 的。