Prhub

#35500 [CI/NPU] Isolate multi-node tests by run_id to prevent concurrent-run…

原始 PR 作者 hhhh1252023 合并时间 2026-09-01 11:08 文件变更 8 提交数 15 评论 8 代码增减 +51 / -10

执行摘要

NPU 多机测试按 run_id 隔离源码与 pod,避免并发互相干扰

PR body 明确说明:"Apply run-scoped isolation across the NPU multi-node test pipeline so that concurrent runs sharing the same k8s namespace and PVC no longer overwrite each other's source code." 并发运行共享同一 k8s namespace 与 PVC 时存在三类问题:源码固定写入 /root/.cache/tests/sglang/ 会互相覆盖;Clear Resources 阶段按 KUBE_JOB_NAME 前缀清理可能误删其他运行的 pod 导致死锁;prepare_cm_data 以宽泛的 app=sgl-ascend label selector 收集 pod 会把其他运行的 pod 写入自己的 ConfigMap。

值得快速浏览,对 CI 并行隔离设计有参考价值:run_id 贯穿"存储路径 + job 名 + pod label + 日志路径"四个层面,是一种轻量且完整的运行级隔离范式。重点借鉴 prepare_cm_data 的"label selector 收窄 + 名称前缀兜底"双保险过滤方式;同时注意模板与 Python 脚本必须同步演进,未来新增 job 类型时要同步补 run-id label。

讨论亮点

cherryblo 的核心关注点是清理阶段的安全性,这与本次隔离目标直接相关:针对 post-test 清理按精确 job 名删除 pod 的方案,cherryblo 指出 "it may cause pods from other parallel tasks to be deleted.",作者回复 "Already rolled back"。另一线程中 cherryblo 对 pre-test bounded retry 给出 "Optimize logging, add delete" 的评价,同样被作者以 "Already rolled back" 回应;对 || true 的改动,作者自评 "it is a useless change" 并回滚。最终合并版本因此收敛为最小改动的隔离方案。

实现拆解

  1. workflow 入口隔离.github/workflows/nightly-test-npu-e2e-multi-node.yml):源码复制目标路径改为 /root/.cache/tests/sglang-${github.run_id}sglang_source_relative_path 同步跟进;KUBE_JOB_NAMEascend-sglang-${inputs.test_type}-test 改为 ascend-sglang-test-${github.run_id},使每个运行拥有全局唯一的 job/pod 前缀;调用 run_npu_e2e_test.py 时新增 --run-id ${github.run_id} 参数,并在测试结束后 rm -rf 本运行的专属源码目录。

  2. 测试编排脚本透传python/sglang/test/ascend/e2e/run_npu_e2e_test.py):run_npu_e2e_test_case 新增 run_id: str = "" 参数并注入 single / multi-pd-mix / multi-pd-separation 三种 k8s_context;CLI 新增 --run-id 参数;prepare_cm_data 接收 run_id 后把 label selector 收窄为 app=sgl-ascend,run-id={run_id}final_kube_job_name 的 pod 名前缀过滤保留作兜底。

  3. k8s 模板统一打标(5 个 jinja2 模板):k8s_single.yaml.jinja2k8s_multi_pd_mix.yaml.jinja2k8s_multi_pd_separation.yaml.jinja2 及两个 green 变体,在每个 pod template 的 labels 中加入 run-id: "{{ run_id }}",与 prepare_cm_data 的 selector 严格对齐,这是整个方案成立的前提。

  4. 日志路径隔离python/sglang/test/ascend/e2e/run_npu_testcase.sh):plog 备份路径加入 ${run_label} 段(run_label 已携带 run_id 前缀),与 log 路径布局一致,避免同一节点上不同运行的 plog 互相覆盖。

  5. 配套与回滚:本 PR 未新增单元测试,正确性依赖 NPU e2e 实跑验证;review 期间提出的 pre-test bounded retry、post-test 精确 job 名清理、|| true 幂等化等增强均被作者回滚,最终合并版本只保留最小的隔离改动。

文件 模块 状态 重要度
python/sglang/test/ascend/e2e/run_npu_e2e_test.py 测试编排 modified 5.88
.github/workflows/nightly-test-npu-e2e-multi-node.yml CI 流水线 modified 4.29
python/sglang/test/ascend/e2e/run_npu_testcase.sh 测试脚本 modified 3.99
python/sglang/test/ascend/e2e/k8s_multi_pd_separation.yaml.jinja2 部署模板 modified 3.72
python/sglang/test/ascend/e2e/k8s_single.yaml.jinja2 部署模板 modified 3.28

关键符号

prepare_cm_data run_npu_e2e_test_case

关键源码片段

python/sglang/test/ascend/e2e/run_npu_e2e_test.py test-coverage

核心隔离逻辑所在:run_npu_e2e_test_case 新增 run_id 参数并注入三种 k8s_context,prepare_cm_data 按 run-id label selector 收窄 pod 收集范围,CLI 新增 --run-id 参数。

# run_npu_e2e_test.py —— 按 run_id 收窄 pod 收集范围
def prepare_cm_data(namespace, pod_string, run_id=None):
    """构造 ConfigMap 数据:{pod_name: pod_ip}。    传入 run_id 时把 label selector 收窄到当前运行,
    避免同一 namespace 下并发运行互相污染 ConfigMap;
    pod_string 过滤(final_kube_job_name)仍作为兜底保障。
    """
    if run_id:
        # 与各 jinja2 模板中写入的 run-id label 保持一致
        label_selector = f"app=sgl-ascend,run-id={run_id}"
    else:
        # 未传 run_id 时保持原来的全量行为,兼容本地 / 非 CI 调用
        label_selector = "app=sgl-ascend"
    pods = core_api.list_namespaced_pod(
        namespace=namespace, label_selector=label_selector
    )
    data = {}
    for pod in pods.items:
        pod_name = pod.metadata.name
        if pod_string in pod_name:
            # 只登记本运行 pod 的名字与 IP,供 ConfigMap 下发
            data[pod_name] = pod.status.pod_ip
    return data
.github/workflows/nightly-test-npu-e2e-multi-node.yml infrastructure

隔离的入口层:源码目录、KUBE_JOB_NAME、--run-id 传参、结束清理都在这条 workflow 中串联,是并发隔离方案能落地到 CI 的关键。

# nightly-test-npu-e2e-multi-node.yml —— run_id 贯穿源码路径与 job 命名
# 复制源码到共享 PVC,目录带 run_id 后缀,并发运行互不覆盖
current_path=$(pwd)
target_path=/root/.cache/tests/sglang-${{ github.run_id }}
rm -rf ${target_path}
mkdir -p ${target_path}
cp -r ${current_path}/* ${target_path}/
​
# job 名携带 run_id,保证清理阶段只命中本运行的前缀
KUBE_JOB_NAME=ascend-sglang-test-${{ github.run_id }}
sglang_source_relative_path=tests/sglang-${{ github.run_id }}# 把 run_id 传给 Python 编排脚本,用于 pod label 与 ConfigMap 过滤
CMD="${CMD} --kube-job-name-prefix ${KUBE_JOB_NAME} \\
  --run-id ${{ github.run_id }}"# 测试结束后清理本运行的专属源码目录,避免 PVC 累积残留
rm -rf /root/.cache/tests/sglang-${{ github.run_id }} || true

评论区精华

post-test 精确 job 名清理方案可能误删并行任务 pod 正确性

cherryblo 指出按精确 job 名删除 pod 的方案 "it may cause pods from other parallel tasks to be deleted.";作者回应 "Already rolled back"。

结论:该增强被回滚,保留原清理逻辑,避免误删其他并行运行的 pod。 · 已解决

pre-test bounded retry 清理增强 设计

cherryblo 对 max 60 次重试的清理逻辑给出 "Optimize logging, add delete" 的建议;作者回复 "Already rolled back"。

结论:该方案未进入合并版本,回滚后保持原有清理与等待逻辑。 · 已解决

kubectl get pods 幂等性改动的必要性 question

cherryblo 询问 `kubectl get pods ... || true` 改动 rationale("Please confirm the rationale behind this change.");作者自评 "it is a useless change" 并回滚。

结论:改动被认定为无效并回滚,workflow 保持原样。 · 已解决

风险与影响

  • 模板与脚本同步风险prepare_cm_data 依赖 pod 上存在 run-id label,必须与 5 个 jinja2 模板同步演进;未来新增 job 类型(如新的 green 变体)若漏加 label,该类型测试将收集不到 pod。
  • PVC 残留累积:收尾的 rm -rf 只在正常流程末尾执行,job 超时或被 kill 时可能残留 /root/.cache/tests/sglang-${run_id} 目录,长时间累积会占用共享盘空间。
  • Job 名长度限制KUBE_JOB_NAMEtest_type 换成 run_id 后变长,PR body 也提到 "within the length limit",后续若再拼接其他标签需注意 k8s 63 字符上限。
  • 兼容性run_id 参数缺省为空时所有行为与改动前一致,本地调试不受影响,风险较低。
  • 对用户:无任何产品功能影响,纯测试/CI 基础设施改动。
  • 对系统:NPU nightly e2e 多机测试可在同一 k8s namespace 与共享 PVC 上并行运行,不再互相覆盖源码、污染 ConfigMap 或误删 pod,CI 稳定性显著提升。
  • 对团队:影响范围限定于使用 nightly-test-npu-e2e-multi-node.yml 流水线的 NPU 多机测试场景;调试者排查并发问题时定位更快。
模板与脚本需同步演进 PVC 残留累积风险 Job 名长度上限约束 无新增单测覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论