执行摘要
- 一句话:为自构建命令的启动器新增快照测试
- 推荐动作:值得精读。重点看
install_shell_recorder 的冻结策略(PID、sleep、makedirs)和 iter_self_executing_launchers 的“按行为发现”设计,它把‘哪些启动器需要保护’变成了可断言的测试。对负责启动脚本重构的工程师,这套快照是正确的第一道防线;后续 PR #1907-#1911 都建立在此基础上。
功能与动机
本 PR 是跟踪 issue #1837“refactoring and enhancements”中的一环(PR body 注明 Part of #1837)。在整理启动脚本时发现,p2p_weight_transfer/run.py、formal_math/run_minimal.py 这类启动器不经过 command_utils,而是自己构建完整命令行并直接调用 subprocess 或 shell 脚本,既有快照测试(针对 shell 启动器和 python 启动器)无法覆盖。测试 docstring 明确指出“These launchers build their whole command line by hand, so only a snapshot pins it”,即只有快照能把这类手写命令固定下来,防止后续重构悄悄改变训练启动命令。
实现拆解
- 扩展测试基架(tests/fast/launch_scripts/py_harness.py):新增
iter_self_executing_launchers,以“不属于 scripts/run_*.py 约定且源码文本包含 ray job submit”的行为特征发现自执行启动器;新增 install_shell_recorder,通过 monkeypatch subprocess.run、time.sleep、os.makedirs、os.getpid/os.getppid 录制命令并冻结不确定性来源,使录制结果可复现。
- 新增快照测试(tests/fast/launch_scripts/test_self_executing_launchers.py):定义
LauncherCase 与参数化 fixture recorded,为 p2p_weight_transfer 的 11 个模型 profile × p2p/broadcast 模式以及 formal_math 的 import 模式共 23 个 case 执行启动器入口并把命令录制结果与快照比较;另含重跑一致性、最终必须 ray job submit、发现覆盖三个维度断言。
- 生成 23 个快照文件(tests/snapshots/launch_scripts/self_executing/):按“启动器路径/场景名.txt”的结构存放完整录制(含环境清理、ray 启动、
ray job submit 携带的完整训练参数),作为 CI 比对基线;这些文件同时充当手写命令的“文档”。
- 配套约束:
TestDiscovery 保证“按行为发现”与“已覆盖 case”完全一致,新增手写启动器会立刻被测试点名;同时显式声明无法被沙箱化的 cmd_prepare 入口点并校验其硬编码 /root/models 路径,避免测试盲区被掩盖。
关键文件:
tests/fast/launch_scripts/test_self_executing_launchers.py(模块 启动器;类别 test;类型 test-coverage;符号 LauncherCase, recorded, TestEverySelfExecutingLauncher, test_commands_match_snapshot): 本 PR 的核心:为自执行启动器新增快照测试与发现断言,是 23 个快照的消费方。
tests/fast/launch_scripts/py_harness.py(模块 测试基架;类别 test;类型 test-coverage;符号 iter_self_executing_launchers, install_shell_recorder, fake_run): 新增录制器与发现函数,是快照测试的基架。
tests/snapshots/launch_scripts/self_executing/examples/infra_features/p2p_weight_transfer/run.py/run/GLM-5/p2p.txt(模块 快照;类别 docs;类型 documentation): 代表性快照,固定了 GLM-5 在 p2p 传输模式下的完整训练命令,同类快照共 23 个。
tests/snapshots/launch_scripts/self_executing/examples/infra_features/p2p_weight_transfer/run.py/run/GLM-5/broadcast.txt(模块 快照;类别 docs;类型 documentation): 与 p2p 对应的 broadcast 模式快照,体现同一启动器不同模式的差异。
关键符号:iter_self_executing_launchers, install_shell_recorder, fake_run, recorded, test_commands_match_snapshot, test_reruns_produce_identical_recordings, test_the_launcher_submits_a_ray_job, test_every_self_executing_launcher_has_at_least_one_case, test_the_uncovered_entrypoint_is_named_and_still_uncoverable
关键源码片段
tests/fast/launch_scripts/py_harness.py
新增录制器与发现函数,是快照测试的基架。
# 自执行启动器:不满足 scripts/run_*.py 约定,但源码里包含 ray job submit 的 Python 文件
def iter_self_executing_launchers() -> list[Path]:
"""Launchers that reach the shell themselves rather than through command_utils."""
roots = [REPO_ROOT / root for root in ("scripts", "examples", "tools")]
convention = {script.path for script in iter_py_launch_scripts()}
return sorted(
path
for root in roots
for path in root.rglob("*.py")
if path not in convention and "ray job submit" in path.read_text(errors="replace")
)
# 这类启动器持有自己的 subprocess 句柄,不会经过 command_utils 的录制器,因此单独拦截 subprocess.run
def install_shell_recorder(monkeypatch, sandbox: Path) -> Recording:
"""A launcher holding its own subprocess handle never touches the recorded command_utils helpers."""
recording = Recording(commands=[], pseudo_files=[])
# fake_run 记录命令并返回一个足够大的 GPU 数量,让启动器里的等待循环立即通过
def fake_run(command, *args, **kwargs):
recording.commands.append(command if isinstance(command, str) else " ".join(command))
return subprocess.CompletedProcess(
args=command, returncode=0, stdout=_GPU_COUNT_ANY_WAIT_LOOP_ACCEPTS, stderr=""
)
monkeypatch.setenv("MILES_LOG_DIR", str(sandbox))
monkeypatch.setattr(subprocess, "run", fake_run)
monkeypatch.setattr(time, "sleep", lambda seconds: None)
monkeypatch.setattr(os, "makedirs", lambda path, **kwargs: None)
monkeypatch.setattr(os, "getpid", lambda: _FROZEN_PID)
monkeypatch.setattr(os, "getppid", lambda: _FROZEN_PPID)
return recording
评论区精华
本 PR 没有人审 review 评论(review_comments_count 为 0),GitHub 上仅有一条 Gemini Code Assist 的停用提示,不构成技术讨论。真正有价值的“讨论”藏在测试代码的 docstring 里,例如“这些启动器完全手写命令行,只有快照能把它钉住”“启动器会把自身 PID 嵌进清理命令,所以快照只有在冻结 PID 后才会稳定”,这些是测试设计约束的自我说明。
风险与影响
- 风险:快照与模型脚本强耦合:23 个快照记录了 p2p_weight_transfer 针对 11 个模型 profile 的完整参数,任何参数默认值、模型脚本路径或配置 key 变化都会让 CI 失败,维护成本集中在 tests/snapshots/launch_scripts/self_executing/ 下。录制有盲区:
install_shell_recorder 只拦截 subprocess.run,若启动器改用 os.system、subprocess.Popen 等路径则录不到;fake_run 固定返回 GPU 数量,依赖真实命令输出的启动器可能被“假阳性”通过。发现机制依赖文本匹配:iter_self_executing_launchers 靠源码里出现 ray job submit 字符串判断,注释或文档字符串误出现可能造成误报/漏报,目前依赖 TestDiscovery 兜底。此外 cmd_prepare 因硬编码 /root/models 无法沙箱化,该入口点没有被录制覆盖。
- 影响:开发者修改任何自执行启动器(尤其是 p2p_weight_transfer 与 formal_math 示例)时必须同步更新对应快照,否则 CI 会失败;CI 新增 23 个快照断言,运行时间略有增加。对团队而言,这套基线把长尾的手写启动命令显式化,使 #1837 系列后续重构(如 #1907 修复 rotary_base、#1910 用 Python 替换 shell 配置)有明确的回归网。对生产系统无影响(纯测试变更)。
- 风险标记:快照维护成本高, 环境敏感测试, 文本发现机制, monkeypatch 依赖, 存在无法覆盖的入口点
关联脉络
- PR #1907 Fix p2p profile's rotary_base not reaching the model script it configures: 同一 p2p_weight_transfer 启动器与快照目录,后续修复了本 PR 快照暴露出的参数传递缺陷。
- PR #1908 Snapshot test the argv of all model scripts: 同一快照测试系列的下一环,将快照覆盖扩展到所有模型脚本 argv。
- PR #1909 Expand the model args in python before building the command: 同一命令构建链路的重构,依赖本 PR 的快照基线验证行为不变。
- PR #1910 Replace the model config shell scripts with python: 同一系列中将 shell 模型配置改造成 Python,本 PR 的快照提供回归保护。
- PR #1911 Quote the model args miles inlines into the launch command: 同一系列中修复内联参数引用问题,同样依赖快照验证。
- PR #2279 Run the launch script snapshot tests by hand instead of in CI: 后续把启动脚本快照测试移出 CI,改为手动执行,直接关联本 PR 新增的测试。
参与讨论