Prhub

#1911 Quote the model args miles inlines into the launch command

原始 PR 作者 fzyzcjy 合并时间 2026-08-09 18:52 文件变更 44 提交数 18 评论 12 代码增减 +104 / -52

执行摘要

为内联 model args 加 shell 引号,修复 MoE 参数被 glob 展开

在 op8-16 把 model args 展开内联进命令后,--moe-layer-freq 这类含方括号的 token 直接拼接进 shell 命令行会被当作 glob 展开(测试注释原话: '--moe-layer-freq [0,0,0,1,1] is a glob; unquoted it expands against the launch directory'),导致训练进程收到错乱甚至静默错误的参数。shell_safe_model_args 的 docstring 也写明: "For callers splicing the args into a command line, where --moe-layer-freq [1,1,1] is a glob."。该 PR 是 #1837 tracking issue 中列出的 #1911 项,PR body 声明依赖 ci-megatron-pr: tom/refactor-miles-repo-megatron/op8-13。

值得精读。这是一个小而精的启动器修复:token 级 shlex.quote + round-trip 测试的设计思路可直接复用——它既解决了 shell 注入/展开风险,又用 'quoted 后 shlex.split 与原 argv 一致' 的判据锁死了转义正确性,避免过度转义。建议重点关注 shell_safe_model_args 的 None 分支设计(把 FSDP 特例收进函数内部)和快照最小 churn 策略(shlex.quote 只在必要时加引号),以及 CI 评论区对 'lane 无真实覆盖' 的反思——它提醒我们绿色 CI 不等于测试真的跑了。

讨论亮点

本 PR 的 PR review 无文字评论(yueming-yuan 直接 APPROVED),但 issue 评论区有作者(fzyzcjy,经 Claude Code 自动发布)关于 CI 的密集诊断,核心交锋如下:

"op8-9's inline base64: payload needs the matching Megatron-LM change, which CI was not told to check out. Fixed by declaring the dependency in the PR description."

"No tests found for hw=CUDA, suite=stage-c-8-gpu-h100 ... So those SUCCESS marks mean 'ran nothing', not 'passed'." —— 指出该 GPU lane 此前从未有真实覆盖,run-ci-image label 强制 --match-all-labels 后才暴露出测试。

关于两个遗留失败:"one is an image regression, the other is a code regression from a specific main commit. Neither is from this chain" —— 通过将 PR 固定到 dev-202607240207 旧镜像做实验,证明 qwen3_5_35b_a3b_lora_ci.py 的 cuDNN 失败是共享镜像(TransformerEngine 2.17 相关)回归,而 fsdp_colocated_2xGPU 超时在旧镜像上仍复现,属 main 代码回归。

"traced it to a python-version-dependent Path.exists() behaviour on the CPU runner" —— 快照测试的 PermissionError 根因是 Path.exists() 的 Python 版本差异,修复后 stage-a/b-cpu 全绿。

实现拆解

实现分四步:

  1. 新增 shell 安全包装函数(miles/utils/external_utils/model_args_utils.py):新增 shell_safe_model_args(model_type: str | None) -> str,内部先调用 load_model_args() 取原始参数字符串,按空白拆分成 token 后用 shlex.quote() 逐个引用再拼接。设计要点是 token 级引用而非整串引用,避免将整段参数折叠成单个 argv;同时把原来的 model_type is None 三元分支收进函数内部,FSDP 等无 megatron 配置的后端返回空字符串,不贡献任何 argv。

  2. 替换全部内联调用点(miles/utils/external_utils/command_utils.py、examples/experimental/formal_math/single_round/run_minimal.py、docker/npu_patch/miles.patch):convert_checkpoint 中 torchrun 拼装的 load_model_args(megatron_model_type) 换成 shell_safe_model_args(megatron_model_type);execute_train 中原来的 load_model_args(...) if megatron_model_type is not None else "" 直接替换为 shell_safe_model_args(megatron_model_type);formal_math 示例与 NPU patch 同步替换,保证所有入口行为一致。

  3. 重新生成 launch 快照(tests/snapshots/launch_scripts/py/scripts/ 下约 20 个 golden 文件):凡包含 --moe-layer-freq 的命令行都加上单引号(如 --moe-layer-freq '[0,0,0,1,1]'),普通参数(如 --num-layers 36)因 shlex.quote 只在必要时转义而保持原样,快照 churn 最小化。

  4. 测试配套(tests/fast/utils/external_utils/test_model_args_utils.py、tests/fast/utils/test_command_utils.py):TestShellSafeModelArgs 覆盖四类行为——需要引号的 glob token、无需引号的普通 token、空 model_type 返回空串、以及 shlex.split 往返一致;test_command_utils 新增两个端到端断言,验证 execute_train 生成的 ray job submit 命令中 --moe-layer-freq 被引号包裹,且 submitted 命令尾部的训练参数区与 load_model_args 声明的 argv 完全一致。

文件 模块 状态 重要度
miles/utils/external_utils/model_args_utils.py 参数加载 modified 6.68
miles/utils/external_utils/command_utils.py 命令工具 modified 5.88
tests/fast/utils/external_utils/test_model_args_utils.py 参数加载 modified 6.38
tests/fast/utils/test_command_utils.py 命令工具 modified 5.41
examples/experimental/formal_math/single_round/run_minimal.py 示例脚本 modified 4.67
docker/npu_patch/miles.patch NPU 补丁 modified 2.91
tests/snapshots/launch_scripts/py/scripts/run_deepseek.py/train.txt 快照文件 modified 1.82

关键符号

shell_safe_model_args convert_checkpoint execute_train

关键源码片段

miles/utils/external_utils/model_args_utils.py data-contract

核心变更文件,新增 shell_safe_model_args(),把 load_model_args 的产物按 token 级 shlex.quote 转义,并吸收 None 分支(FSDP 场景返回空串)。

# miles/utils/external_utils/model_args_utils.pydef shell_safe_model_args(model_type: str | None) -> str:
    """为把 args 拼进命令行的人准备的转义版本:--moe-layer-freq [1,1,1] 在 shell 里是 glob。"""
    if model_type is None:
        # FSDP 等后端没有 megatron 模型配置,必须不贡献任何 argv;
        # 这里把原来调用点的三元分支收进函数内部,调用方不再需要记忆这个特例。
        return ""
    # 先按空白拆分再逐 token 引用,而不是整体引用:
    # 整体引用会把整段参数变成单个 argv,训练进程将无法解析。
    # shlex.quote 只在 token 含特殊字符时加引号,普通参数(如 --num-layers 36)
    # 保持原样,快照不会无谓 churn。
    return " ".join(shlex.quote(token) for token in load_model_args(model_type).split())
miles/utils/external_utils/command_utils.py dependency-wiring

两个主要调用点 convert_checkpoint 与 execute_train 从 load_model_args 切换到 shell_safe_model_args,是行为变更的落地入口。

# miles/utils/external_utils/command_utils.py 中 execute_train 的 ray job 提交路径if get_bool_env_var("MILES_SCRIPT_ENABLE_RAY_SUBMIT", "1"):
    # 旧代码是 load_model_args(...) if megatron_model_type is not None else "",
    # None 特例现在由 shell_safe_model_args 内部消化,语义不变但更内聚。
    model_args = shell_safe_model_args(megatron_model_type)
    exec_command_cpu(
        f"export no_proxy=127.0.0.1 && export PYTHONUNBUFFERED=1 && "
        f"ray job submit --address=http://127.0.0.1:8265 "
        f"--runtime-env-json={shlex.quote(runtime_env_json)} "
        f"-- python3 {train_script} "
        f"{model_args} " # 含 [0,0,0,1,1] 的 token 已被引号包裹
        f"{train_args}"
    )
tests/fast/utils/test_command_utils.py test-coverage

新增两个集成测试,验证 execute_train 实际生成的提交命令中 --moe-layer-freq 被引用,且 round-trip 后 argv 与模型脚本声明一致,防止回归。

# tests/fast/utils/test_command_utils.pydef test_model_args_survive_a_shell_round_trip_unchanged(self, commands):
    """引号正确的判据:shlex.split 拆回的 argv 必须与模型脚本声明完全一致。
    既不能让 glob 把 [0,0,0,1,1] 展开成文件列表,也不能因过度转义改变参数值。
    """
    command_utils.execute_train(
        train_args="", num_gpus_per_node=8, megatron_model_type="deepseek-v3-5layer"
    )
​
    declared = load_model_args("deepseek-v3-5layer").split()
    submitted = shlex.split(commands[-1])
​
    # 只比对命令尾部的训练参数区(头部还有 ray job submit 等前缀),
    # 保证训练进程收到的 argv 与模型脚本声明逐 token 相等。
    assert submitted[len(submitted) - len(declared):] == declared

评论区精华

base64 内联 payload 的跨仓库依赖未声明 正确性

op8-9 将临时文件参数改为 base64: 内联后,--te-precision-config-file 由 Megatron-LM 消费,CI 未 checkout 对应分支导致 OSError: [Errno 36] File name too long: 'base64:...'。

结论:在 PR body 声明 ci-megatron-pr: tom/refactor-miles-repo-megatron/op8-13 依赖后,stage-c-4-gpu-h200 8/8 通过,确认修复。 · 已解决

stage-c-8-gpu-h100 此前从未有真实覆盖 测试

无 label 的 PR 在该 lane 上运行 run_suite.py 输出 'No tests found ... No tests to run. Exiting with success.',只有本 PR 带 run-ci-image label(强制 --match-all-labels)才真正执行测试;作者据此收回 ' 同链 PR 同 lane 通过 ' 作为证据的论证。

结论:该 lane 历史 SUCCESS 均为空跑,失败无法用排除法归因到本 PR;此讨论暴露了 CI label 体系的覆盖盲区。 · 已解决

两个遗留 GPU 失败为 pre-existing 但机制不同 正确性

test_qwen3_5_35b_a3b_lora_ci.py(cuDNN CUDNN_STATUS_BAD_PARAM)与 test_qwen3_0.6B_fsdp_colocated_2xGPU.py(1800s 超时)在 main 2026-07-28 nightly 同样失败;镜像 pin 实验进一步拆分:旧镜像 dev-202607240207 上 lora 用例通过(镜像回归),fsdp 超时依旧(main 代码回归)。

结论:两个失败均非本链引起,且机制不同;PR 结论为 ' 与 main 当前状态同样绿 ',image pin 实验后回滚。 · 已解决

Path.exists() 的 Python 版本依赖导致 CPU runner 快照测试失败 正确性

stage-a-cpu 报 PermissionError: [Errno 13] Permission denied: '/root/models/DeepSeek-V4-Flash-FP8-bf16/model.safetensors.index.json',追溯为 python 版本相关的 Path.exists() 行为;第一版修复连 checkout 也隐藏了,导致 load_model_args 的 AssertionError,随后修正为只隐藏产物路径。

结论:修复后 stage-a-cpu 全部 shard 与 stage-b-cpu 全绿。 · 已解决

镜像 pin 实验定位两个失败 other

作者将 PR 固定到最后一次双 lane 均通过的 dev-202607240207,实验结果显示 lora 用例不再出现 fused_attn 报错而 fsdp 仍超时;同时旧镜像缺少 mooncake overlay 导致 ImportError: FieldSchema。

结论:实验成功拆分两个失败机制,随后回滚 pin,不影响本 PR 合并。 · 已解决

风险与影响

  1. 启动命令文本契约变更:execute_train 与 convert_checkpoint 生成的命令行字符串发生变化(带引号的 --moe-layer-freq),依赖这些命令原始文本的外部工具、CI 门禁或用户脚本需同步适配;快照已同步更新,但仓库外消费者无法感知。
  2. 跨仓库合并顺序约束:op8-9 引入的 base64: 内联 payload 依赖 Megatron-LM 分支 tom/refactor-miles-repo-megatron/op8-13 的 resolve_file_arg,本 PR body 已声明该依赖,但若 Megatron 侧未先合并,CI 会出现 ENAMETOOLONG 类失败。
  3. 转义语义边界:shlex.quote 按 POSIX shell 语义转义,若某模型 args token 本身含单引号(嵌套引号),转义后 round-trip 虽保持正确,但命令可读性下降;当前测试只覆盖 deepseek-v3-5layer 一种含方括号场景,其他特殊字符(通配符 *、分号、$ 等)未被快照显式锁定。
  4. NPU 侧同步风险:docker/npu_patch/miles.patch 同步了替换,但 patch 文件本身不经过 fast 测试直接验证,若上游代码在 patch 生成后又变化会产生漂移。

影响范围集中在 launch 命令生成路径:所有经 command_utils.execute_train 提交的 ray job 训练命令、convert_checkpoint 的 torchrun 转换命令、formal_math 示例、以及 NPU 镜像 patch 中的命令拼装。对用户而言,含方括号列表的 MoE 参数(--moe-layer-freq)、含特殊字符的路径等此前可能被 shell glob 展开导致静默错误,现在会被正确引用;普通参数命令文本不变,快照 diff 因此非常收敛。对团队而言,新增的 round-trip 测试为模型 args 内联机制提供了可回归的契约保障,后续修改 scripts/models/*.py 模型定义时若破坏 argv 语义会被 CI 立即拦截。

启动命令文本契约变更 跨仓库依赖需先合并 镜像 patch 无直接测试覆盖 快照基线大范围更新

关联 Issue

#1837 Tracking issue for refactoring and enhancements

完整报告

参与讨论