Prhub

#56597 [CI/Build] Retry CPU image stream interruptions

原始 PR 作者 khluu 合并时间 2026-09-15 12:18 文件变更 1 提交数 3 评论 3 代码增减 +1 / -1

执行摘要

扩展 CPU 镜像构建重试正则,覆盖下载流中断

PR body 明确说明这是对 #55317 引入的重试逻辑的扩展:「This extends the retry logic introduced by #55317.」此前的分类器只认识 registry/DNS/TLS 层抖动,认不出下载流被中断这一类失败。证据链非常具体:Main #88718 的 CPU Language shard 1「later lost the same 1.8-GiB LLVM download with BrokenPipeError, but the classifier rejected it and exited after attempt 1」;9 月 15 日 Main #88996 在 aicf-jf-cicd-01 上丢掉三个 CPU job(两次 triton-cpu clone 以 HTTP/2 cancellation/early EOF 结束,一次 LLVM 下载以 BrokenPipeError 结束),Main #88997 在同一宿主同一时刻丢掉全部三个 CPU Language shard,Main #88999 又新增第八个失败。作者强调「Each job reached the existing classifier, none matched, and each exited after attempt 1」,并且这些失败都发生在测试之前、对应的 PR 构建此前已通过 CPU lane,因此可归因于共享传输层而非某个源码 diff。

建议 5 分钟内快速浏览即可,不必精读:改动是一行正则,实现意图清晰,风险边界也已被作者在 body 中显式说明。值得关注的设计决策有三点:一是「复用同一个有界重试框架、只扩模式不提上限」这一取舍,把不确定性风险封在 4 次尝试 / 70 秒退避内;二是签名集合来自真实失败日志(8 个样本逐条反推),而非凭空枚举;三是它暴露了 vLLM CI 中「靠长正则做故障分类」这一模式的脆弱性——若后续再出现同类 PR,建议推动把 BUILD_RETRY_PATTERN 拆成可测试的分类函数,并补一份「样例行命中 / 编译错误不命中」的脚本级测试。此外,读者应留意重试逻辑只能缓解 aicf-jf-cicd-01 这类宿主侧问题,根因排查不应止步于本 PR。

讨论亮点

本 PR 的讨论量很小(3 条 issue 评论、3 条 review、0 条行级 review 评论),有效信息集中在作者自述的验证边界与维护者的批准动作上:

claude[bot](两次 COMMENTED):「This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.」——即自动 review 未真正执行,模式边界的把关完全落在人工侧。

tlrmchlsmth:APPROVED(无正文、无行级评论)——最终以维护者批准合入,未留下任何反对或需要修改的意见。

khluu(issue 评论):「Human line review remains required before readiness.」以及「the external failure paths cannot be forced deterministically」——作者自己承认新增行必须人工逐行确认,且无法在本地确定性复现这些外部网络失败路径。

khluu(issue 评论):「This head covers the eight pre-test failures now observed on physical aicf-jf-cicd-01 across unrelated mains #88996/#88997/#88999: seven truncated Git/HTTP clone streams plus one Triton LLVM BrokenPipeError.」——这是本 PR 最有价值的一条证据归纳,说明签名集合是从真实失败样本反推出来的,而非凭经验穷举。

另外 body 中提到一份短暂开启的重复草稿 #56938 在验证前关闭,其独有的流签名被合并进本 PR,体现了同类 CI 修复的去重与收敛。

实现拆解

本 PR 只有 +1/-1,全部改动集中在 .buildkite/scripts/hardware_ci/run-cpu-test.sh 第 59 行附近的 BUILD_RETRY_PATTERN 变量上,属于「只扩模式、不动控制流」的最小改动。

  1. 变更入口:run-cpu-test.sh 在 docker pull ubuntu:25.04 || true 与 echo "--- :docker: Building Docker image" 之后、build_log="$(mktemp)" 之前定义 BUILD_RETRY_PATTERN,这是镜像构建重试循环唯一的判定依据,因此改这一个字符串就能改变全部分类行为。

  2. 模式扩展分三类:新增 6 个备选项,覆盖此前未识别的失败形态——
    - Python 下载器层:The read operation timed out;
    - 管道断裂层:BrokenPipeError:.*Broken pipe(对应 1.8 GiB LLVM 包拉取中断);
    - Git/HTTP 传输层:HTTP/[0-9.]+ stream [0-9]+ was not closed cleanly、fetch-pack: unexpected disconnect、fatal: early EOF、invalid index-pack output(对应 triton-cpu clone 被 HTTP/2 取消后的一串连带报错)。
    这些项被追加在原 11 项(dial tcp、i/o timeout、failed to authorize、TLS/DNS/Connect 类错误、Failed to fetch: 等)之后,保持原有语义不变。

  3. 控制流与预算完全保留:BUILD_MAX_ATTEMPTS=4(1 次初始 + 3 次重试)与 BUILD_RETRY_WAITS=(10 20 40) 未做任何改动。作者在 body 中明确「preserve the existing four-attempt limit and 10/20/40-second backoff」,即用「有界重试」把新增模式的爆炸半径限制在最坏多等约 70 秒,而不是让瞬时故障无限消耗 CI 资源。

  4. 提交演进(3 个 commit,逐步加签名的过程):ce07d29 先加读超时;feddb517 追加断管签名;3248f775(head)追加截断 Git 流签名。三个 commit 分别由 OpenAI Codex 与 Sherlock 协作署名,呈现「从日志里发现一种失败 -> 补一条签名 -> 再发现下一种」的迭代式修复路径,而不是一次性设计。

  5. 测试与验证配套:本 PR 没有源码/测试联动信号,也没有新增自动化测试。作者在 body 中给出的验证手段是 bash -n、逐条比对捕获到的 timeout / broken-pipe / HTTP2 / early-EOF / index-pack 日志行确实命中 BUILD_RETRY_PATTERN、并验证一条代表性编译错误不命中该模式,再加 shellcheck、pre-commit 与 git diff --check;线上验证是 Buildkite #89005(head 3248f7752f11 精确解析),bootstrap、Buildkite pre-commit、GitHub pre-commit bridge 与 CPU image gate 均通过。

文件 模块 状态 重要度
.buildkite/scripts/hardware_ci/run-cpu-test.sh 镜像构建 modified 3.35

关键符号

BUILD_RETRY_PATTERN BUILD_MAX_ATTEMPTS BUILD_RETRY_WAITS

关键源码片段

.buildkite/scripts/hardware_ci/run-cpu-test.sh configuration

本 PR 唯一改动文件,也是 CPU 镜像构建重试逻辑的全部实现位置:`BUILD_RETRY_PATTERN` 决定哪些失败允许重试,`BUILD_MAX_ATTEMPTS` / `BUILD_RETRY_WAITS` 决定重试预算,直接决定 CPU lane 在宿主网络抖动下的存活率。

# .buildkite/scripts/hardware_ci/run-cpu-test.sh
# CPU 镜像构建阶段:本 PR 唯一改动点,就在下面这一条模式字符串上。
​
docker pull ubuntu:25.04 || true
​
# building the docker image
echo "--- :docker: Building Docker image"
​
# 以 | 分隔的瞬时故障签名表,日志命中才允许重试。
# 前 11 项(dial tcp ... Failed to fetch:)由 #55317 引入,覆盖 registry / DNS / TLS 层抖动,本 PR 未改动;
# 后 6 项为本 PR 新增,覆盖此前被漏判的“下载流被中断”一类失败:
# 1) The read operation timed out -> Python 下载器读超时
# 2) BrokenPipeError:.*Broken pipe -> 1.8 GiB LLVM 包拉取时管道断裂
# 3) HTTP/<ver> stream <id> was not closed cleanly -> Git / HTTP2 流被取消
# 4) fetch-pack: unexpected disconnect -> Git 传输层断连
# 5) fatal: early EOF -> triton-cpu clone 提前结束
# 6) invalid index-pack output -> 上述截断的连带报错
BUILD_RETRY_PATTERN='dial tcp|i/o timeout|failed to authorize|TLS handshake timeout|connection reset|PROTOCOL_ERROR|Could not resolve host|Temporary failure in name resolution|dns error: failed to lookup address information|client error \(Connect\)|error sending request for url|Failed to fetch:|The read operation timed out|BrokenPipeError:.*Broken pipe|HTTP/[0-9.]+ stream [0-9]+ was not closed cleanly|fetch-pack: unexpected disconnect|fatal: early EOF|invalid index-pack output'
​
# 重试预算与退避节奏保持不变,仍然是“有界重试”:
# 1 次初始构建 + 3 次重试,重试前分别等待 10 / 20 / 40 秒。
BUILD_MAX_ATTEMPTS=4 # 1 initial + 3 retries
BUILD_RETRY_WAITS=(10 20 40) # seconds to wait before retry 1/2/3
​
build_log="$(mktemp)"
​
# 说明:其后的重试循环(读取 build_log、按 BUILD_RETRY_PATTERN 判定、命中则按
# BUILD_RETRY_WAITS 退避后重试、超出 BUILD_MAX_ATTEMPTS 即失败)在本 PR 中未做任何改动,
# 因此“瞬时故障 vs 确定性故障”的判别责任全部压在上面那一条正则上。

评论区精华

新增正则的误匹配边界需人工逐行确认,但自动 review 不可用 正确性

claude[bot] 两条 COMMENTED 均表示「This pull request is from a fork — automated review is disabled」,自动审查实际未运行;仓库维护者 tlrmchlsmth 直接 APPROVED,未留下任何行级评论。作者在 body 中写明「a human maintainer must review every newly amended line before this draft is marked ready」,并在 issue 评论中重申「Human line review remains required before readiness」。

结论:改动最终由 tlrmchlsmth 批准合入,没有反对意见,但也没有任何行级讨论记录;模式边界的确认完全依赖作者自测(`bash -n`、shellcheck、逐条样例行命中、代表性编译错误不命中)。 · 已解决

外部失败路径无法确定性复现,验证只能依赖线上 CI 测试

作者自述「Exact CPU PR validation of current head `3248f7752f` is pending; the external failure paths cannot be forced deterministically.」验证方式退化为:历史失败日志抽样比对 + Buildkite #89005 在精确 head 上跑通 CPU image gate。body 中列出 Sep 15 在 `aicf-jf-cicd-01` 上的 8 次失败作为证据链。

结论:以线上 CI 通过加历史日志抽样作为等价验证;未新增可离线复现的回归测试,假阳性(误把确定性失败当瞬时故障)与假阴性(日志换行导致漏匹配)仍无自动化守护。 · 未解决

只扩模式不改预算:复用 #55317 的有界重试框架 设计

PR body 明确「preserve the existing four-attempt limit and 10/20/40-second backoff」,新签名被并入同一个 `BUILD_RETRY_PATTERN`,而不是新建第二套重试路径或提高上限。这个取舍把新增模式的最坏代价限制在约 70 秒退避加一次重复拉取。

结论:设计上选择以「有界重试」控制爆炸半径,代价是分类器变成一条 17 个备选项的超长正则,维护性与可测试性下降。 · 已解决

风险与影响

  1. 正则误匹配、掩盖确定性失败:fatal: early EOF 与 invalid index-pack output 既可能来自瞬时断流,也可能来自仓库侧确定性故障(例如 pin 的 commit 不可达、镜像源被改写)。此时重试不会成功,只会把失败延迟约 70 秒并重复拉取大体积产物(LLVM 包约 1.8 GiB),排障时容易误判为「网络抖动」。
  2. 缺少自动化守护:整个分类器是一条横跨 17 个 | 备选项的超长单行字符串,没有任何测试断言「样例行命中 / 编译错误不命中」。后续任何人格式化或改写该行(例如被 shellcheck 提示拆分、误改转义)都可能静默破坏分类能力,而 CI 只会在下次真实断流时才暴露问题。
  3. 正则细节脆弱性:BrokenPipeError:.*Broken pipe 依赖两个片段出现在同一行,日志换行输出时会漏匹配;HTTP/[0-9.]+ stream [0-9]+ was not closed cleanly 中的 [0-9.]+ 语义偏宽(也会匹配 1.2.3),虽然实际日志中无害,但说明模式是从样本行直接抄录而非按语义构造。
  4. 治标不治本:失败集中在 aicf-jf-cicd-01 这一台宿主,属于宿主网络/传输层质量问题,重试只是提高容错概率,未解决根因;若宿主持续劣化,重试会放大 CI 总耗时。
  5. 安全/兼容性风险可忽略:改动不涉及凭据、镜像来源或产物校验逻辑,不改变构建内容,也不影响推理路径。

影响面限定在 CI 的 CPU 镜像构建阶段(.buildkite/scripts/hardware_ci/run-cpu-test.sh 一处),不涉及 vLLM 运行时、模型加载或服务接口。

  • 对开发者:直接收益是 CPU Language 等 lane 因下载流中断而产生的假失败会显著减少(作者已用 8 个真实失败样本验证覆盖),减少重跑 CI 与人工排查成本;代价是新增签名被命中时会多消耗 10/20/40 秒退避与重新拉取时间。
  • 对 CI 系统:有界重试(4 次上限)意味着最坏情况下单个 job 额外耗时约 1 分钟级别,不会出现无限重试耗尽构建机资源的情况;但宿主 aicf-jf-cicd-01 仍可能成为整批 CPU job 同时失败的瓶颈点,本 PR 未消除该单点依赖。
  • 对团队:这是一次典型的「把运维经验固化成分类器签名」的动作,会形成一条持续维护成本——后续每出现一类新的传输中断签名都需要再加一项,长期看更适合改造成带样例的测试化分类器。
缺少自动化回归测试 长正则误匹配风险 可能掩盖确定性失败 仅缓解宿主网络问题

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论