Prhub

#2355 [fix] fix the bugs/outdated commands in `.sh` scripts and the corresponding snapshots

原始 PR 作者 yueming-yuan 合并时间 2026-08-12 10:12 文件变更 26 提交数 20 评论 0 代码增减 +104 / -76

执行摘要

修复启动脚本批量 bug 并同步快照与测试 harness

PR body 开门见山:这批 launcher 每一个都“cannot do what it says”——声称能做某事却实际跑不通。更关键的是,快照套件(#1899/#1901)把坏掉的输出固化成了期望输出,而不是标记错误:“The snapshot suite added in #1899/#1901 recorded the broken output rather than flagging it, so the fixes are visible here as snapshot diffs — one diff per fix, nothing else.” 清理必须先于接下来的 Python 重写(#2356),因为 argv 对比必须基于已经能正常工作的 launcher:“the argv comparison in the next PR is against launchers that already work, so a difference there means the port drifted, not that it inherited a bug.”

值得精读。三个看点:一是 before_ray_job_submit 钩子如何在共享执行器中处理“集群就绪后”的准备工作,这是对多节点训练启动时序的通用解法;二是 os.environ.get(name, default) 默认值急切求值这个隐蔽坑;三是测试 harness 保存/恢复环境以消除顺序依赖的通用做法。建议与 #2354、#2356 连起来读,理解整个脚本清理与重写脉络,并关注 #2356 的 argv 对比是否真的发现移植漂移。

讨论亮点

该 PR 审查过程零评论:guapisolo 直接 APPROVED,没有展开任何讨论。设计权衡的论证全部沉淀在 20 个 commit 的 message 中,最有信息量的几条:d654bad 解释了为何“手动起好集群再跑 launcher”也不可行——execute_train 的 preamble 会先 ray stop --force 再拉起全新单节点 head,导致多节点转换后跟单节点训练错配;97b136e 指出 os.environ.get 的默认参数被急切求值,即使调用者已设置 NCCL_NVLS_ENABLE 也会无条件 shell out 到 nvidia-smi,包括 ROCm 机器;3a9c722 给出了环境泄漏的复现路径(先跑 test_py_launch_scripts.py 再跑 test_model_args.py 会失败 8 个快照,pytest-randomly 下任何顺序都可能触发);21a1f43 则体现作者的注释哲学——理由属于 commit message,不属于调用点上方的四行注释。

实现拆解

1. shell launcher 直接错误修复

逐个点名修复:run-qwen3-next-80B-A3B.sh 的 --runtime-env-json 在 NCCL_NVLS_ENABLE 后漏了逗号,ray 收到无效 JSON;run-gpt-oss-20b-bf16.sh 读取 ${HAS_NVLINK} 却从不探测 NVLink,导致向 NCCL 传入空值;run-qwen3-4B-base-sft.sh、run-qwen3-235B-A22B-sft.sh、run-gpt-oss-20b-bf16.sh 展开从未声明的 ${EVAL_ARGS[@]},脚本一执行就退出;run-kimi-k25.sh 传入 miles 中已不存在的 --filter-zero-reward-samples;run-glm4.5-355B-A32B.sh 把 rollout 数据 dump 到个人绝对路径,并把 NVLink 探测结果写进从未读取的变量。

2. Python launcher 静默失效修复

run_glm47_flash.py 的 --eval-interval 被注释掉,而该 flag 默认是 None,导致它构建的 eval 块从未运行,本 PR 恢复 --eval-interval 20。run_qwen3_30b_a3b.py 与 amd/run_qwen3_30b_a3b.py 在调用 U.execute_train 时漏传 config=args,导致 cuda_core_dump 与 extra_env_vars 被静默丢弃,本 PR 补回。run_glm45_355b_a32b.py 的 hardware 默认值是 H100——恰好是 _execute_train 断言拒绝的值,快照 harness 不得不覆盖它才能运行脚本,现改为 GB200 后该覆盖被移除。

3. ray 生命周期重构(最大变更)

run_deepseek.py 与 run_deepseek_v32.py 的链式 prepare 流程原本是:_prepare_download → _prepare_bf16_ckpt → _prepare_megatron_ckpt → _prepare_cp → _execute_train。但 _prepare_megatron_ckpt 与 _prepare_cp 通过 exec_command_multi_node fan out,其第一步就是 ray.init(address="auto")——在干净主机上会死在 launcher 自己执行 ray start --head 之前;手动起集群也不行,因为 execute_train 的 preamble 先 ray stop --force 再拉全新 head,造成多节点转换之后跟单节点训练错配。修复方式是为 U.execute_train 增加 before_ray_job_submit 回调参数,在 ray 集群就绪、ray job submit 之前执行这些步骤:run_deepseek.py 的 train 通过 functools.partial 注入 _prepare_ray_dependent,run_deepseek_v32.py 的 full_train 注入 _prepare_megatron_ckpt。

4. 共享执行器与测试 harness 修复

miles/utils/external_utils/command_utils.py 中 os.environ.get("NCCL_NVLS_ENABLE", str(int(check_has_nvlink()))) 的默认值会被急切求值:即使调用方已显式设置该变量,每次启动仍会 shell out 到 nvidia-smi 探测 NVLink,在 ROCm 机器上毫无意义。改为 get() or 探测 后只在未设置时才探测。tests/fast/launch_scripts/py_harness.py 的 call_entrypoint 现在保存并在 finally 中恢复 os.environ,解决 run_inkling.py 导出的 MODEL_ARGS_NUM_LAYERS 泄漏到后续测试的问题(未修复时先跑 test_py_launch_scripts.py 再跑 test_model_args.py 会失败 8 个快照)。

5. 配套同步与测试

26 个快照 diff 每个对应一个修复并逐行核对只含预期变更;docs/models/deepseek/deepseek.md 恢复了被 #2391 替换的 V3 页面(run_deepseek.py 仍是 deepseek-v3 唯一 launcher),并补充 external-ray 流程说明(MILES_SCRIPT_EXTERNAL_RAY=1);tests/e2e 的 mimo MTP-only-grad 用例标记 FIXME;顺手补上了 #2300 新增 dashboard 参数导致的快照失配。验证:pytest tests/manual/launch_scripts tests/fast/launch_scripts tests/fast/utils/test_command_utils.py 全量 594 通过,随机顺序下 524 也通过。

文件 模块 状态 重要度
scripts/run_deepseek.py 启动脚本 modified 6.79
scripts/run_deepseek_v32.py 启动脚本 modified 6.23
miles/utils/external_utils/command_utils.py 命令工具 modified 5.39
tests/fast/launch_scripts/py_harness.py 测试夹具 modified 4.99
scripts/run_glm45_355b_a32b.py 启动脚本 modified 4.49
scripts/run_qwen3_30b_a3b.py 启动脚本 modified 4.35
scripts/amd/run_qwen3_30b_a3b.py 启动脚本 modified 4.35
scripts/run_glm47_flash.py 启动脚本 modified 4.32
scripts/run-qwen3-4B_4xgpu.sh 启动脚本 modified 3.48
scripts/run-gpt-oss-20b-bf16.sh 启动脚本 modified 3.3

关键符号

_execute_train _prepare_ray_dependent full_train train execute_train call_entrypoint execute

关键源码片段

scripts/run_deepseek.py core-logic

DeepSeek-V3 唯一 launcher。引入 before_ray_job_submit 钩子并将 _prepare_megatron_ckpt/_prepare_cp 移入该回调,修复多节点 checkpoint 转换因 ray 集群未就绪而必然失败的问题,是本次最大的一处重构。

# scripts/run_deepseek.py
# 核心修复:把依赖 ray 集群的 checkpoint 转换 / 拷贝步骤推迟到集群就绪之后。
# 旧流程在 train 命令里顺序执行 _prepare_megatron_ckpt / _prepare_cp,
# 这两个函数通过 exec_command_multi_node fan out,第一步就是
# ray.init(address="auto"),在干净主机上会先于 launcher 自己的
# ray start --head 崩溃;即便手动起集群,execute_train 的 preamble 也会
# 先 ray stop --force 再拉全新 head,导致多节点转换与单节点训练错配。def _execute_train(args: ScriptArgs, before_ray_job_submit=None):
    # 此处省略 ckpt_args / rollout_args / optimizer_args 等参数串组装
    train_args = (
        f"{ckpt_args} "
        f"{rollout_args} "
        f"{optimizer_args} "
        f"{grpo_args} "
        f"{U.get_default_wandb_args(__file__, run_id=args.run_id)} "
        f"{perf_args} "
        f"{eval_args} "
        f"{sglang_args} "
        f"{misc_args} "
        f"{args.extra_args} "
    )
    U.execute_train(
        train_args=train_args,
        config=args,
        num_gpus_per_node=args.num_gpus_per_node,
        megatron_model_type=args.megatron_model_type,
        extra_env_vars={**sglang_extra_env_vars},
        megatron_path=args.megatron_path,
        # 新增钩子:execute_train 在 ray start --head 成功之后、
        # ray job submit 之前回调,保证多节点步骤拿到可用集群地址
        before_ray_job_submit=before_ray_job_submit,
    )
​
​
@app.command()
@U.dataclass_cli
def train(args: ScriptArgs):
    _prepare_download(args)
    _prepare_bf16_ckpt(args)
    # ray 依赖的步骤不再直接链式调用,而是作为回调注入
    _execute_train(args, before_ray_job_submit=partial(_prepare_ray_dependent, args))
​
​
def _prepare_ray_dependent(args: ScriptArgs):
    """ray 集群就绪后才执行的多节点准备工作。"""
    _prepare_megatron_ckpt(args)
    _prepare_cp(args)
miles/utils/external_utils/command_utils.py core-logic

共享执行器 execute_train 是所有 python launcher 的公共出口。修复 NCCL_NVLS_ENABLE 默认值急切求值导致的每次启动都探测 NVLink 的副作用。

# miles/utils/external_utils/command_utils.py
# NCCL_NVLS_ENABLE 默认值修复。
# os.environ.get(name, default) 的 default 表达式是急切求值的:
# 即便调用者已显式设置 NCCL_NVLS_ENABLE,check_has_nvlink() 仍会被执行,
# 每次启动都 shell out 到 nvidia-smi 做探测,在 ROCm 上无意义。
# 改为 get() or 探测 后,只有环境变量未设置(或为空串)时才探测。runtime_env_vars = {
    "PYTHONUNBUFFERED": "1",
    # 显式设置时直接采用,未设置时才探测 NVLink
    "NCCL_NVLS_ENABLE": os.environ.get("NCCL_NVLS_ENABLE") or str(int(check_has_nvlink())),
    **{
        k: os.environ[k]
        for k in ("NCCL_SOCKET_IFNAME", "GLOO_SOCKET_IFNAME", "NCCL_DEBUG", "NCCL_DEBUG_FILE")
        if k in os.environ
    },
    # 其余 runtime env 键(MASTER_ADDR、CUDA_COREDUMP 系列等)从略
    **extra_env_vars,
    **_parse_extra_env_vars(config.extra_env_vars),
}
runtime_env_json = json.dumps({"env_vars": runtime_env_vars})
tests/fast/launch_scripts/py_harness.py test-coverage

快照 harness 修复:call_entrypoint 在调用前后保存并恢复 os.environ,消除 launcher 导出变量对后续测试的泄漏,这是随机顺序下测试全绿的关键。

# tests/fast/launch_scripts/py_harness.py
# 修复环境变量泄漏:launcher 可能导出自己的配置,如 run_inkling.py
# 会设置 MODEL_ARGS_NUM_LAYERS 供其剪枝变体使用,旧 harness 从不恢复,
# 导致后续所有快照记录依赖“前一个运行的是哪个 launcher”。
# 复现:先运行 test_py_launch_scripts.py 再运行 test_model_args.py
# 会使 8 个 model-args 快照失败;pytest-randomly 下任何顺序都可能触发。def call_entrypoint(module: ModuleType, name: str, overrides: dict[str, object], sandbox: Path) -> None:
    entrypoint = getattr(module, name)
    first = next(iter(inspect.signature(entrypoint).parameters.values()), None)
    saved_env = dict(os.environ)
    try:
        with host_filesystem_frozen(sandbox):
            if first is not None and first.name == "args":
                entrypoint(module.ScriptArgs(**overrides))
            else:
                entrypoint(**overrides)
    finally:
        # 泄漏的开关会让后续记录依赖 launcher 执行顺序,这里强制还原
        os.environ.clear()
        os.environ.update(saved_env)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 核心启动路径变更:before_ray_job_submit 重构改变了 run_deepseek.py / run_deepseek_v32.py 的执行顺序,checkpoint 转换从“launcher 直连”变为“execute_train 集群就绪后回调”;若 ray 启动失败,转换步骤不会执行,多节点用户必须按新文档使用外部集群模式(MILES_SCRIPT_EXTERNAL_RAY=1)并在 launcher 之前自行组集群。
  2. 默认行为变化:run_glm45_355b_a32b.py 的 hardware 默认值从 H100 改为 GB200,未显式传参的 H100 用户会拿到不同配置;run-qwen3-4B_4xgpu.sh 未设置 WANDB_KEY 时不再启用 wandb,依赖默认 wandb 的用户需显式设置;NCCL_NVLS_ENABLE 从“总是探测”变为“尊重显式设置”,空字符串环境变量仍会触发探测。
  3. 快照批量更新:26 个快照 diff 虽逐行核对,但快照测试的特性是“固化当前行为”,若某处修复本身有误,快照会同步固化错误;且 tests/manual 不在 CI 发现路径(#2279),回归可能漏网。
  4. 审查风险:0 条 review 评论、单一 approve,多脚本行为变更缺少第二轮审查视角。

影响范围:scripts/ 下 10 余个 launcher、共享执行器 miles/utils/external_utils/command_utils.py、测试 harness 与 26 个快照、docs/models/deepseek 文档。用户侧收益:DeepSeek-V3 多节点训练从“启动即失败”变为可用,GLM-4.7 flash 评估恢复,AMD Qwen3-30B 恢复 cuda_core_dump 与 extra_env_vars 支持,4 卡共置的 Qwen3-4B 脚本不再因缺 --num-gpus-per-node 而错配资源。测试侧收益:harness 修复消除了测试顺序敏感性,随机顺序下 524 个测试通过,CI 更稳定。团队侧收益:为 #2356 的 Python 重写提供了干净基线,快照体系从此能真正发现回归而非固化错误。影响程度中等偏上,集中在启动脚本与快照测试体系。

核心启动路径变更 快照批量更新 默认参数行为变化 无 review 讨论 外部 ray 集群依赖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论