CI/CD for ML
这一课你将能做到
- 说清 ML 的 CI 比后端多出的三类关卡:数据校验、训练冒烟、指标门禁
- 写 GitHub Actions 工作流:PR 上跑 lint + 冒烟训练并把指标贴回评论,main 合并自动构建镜像
- 用 environment 审批把「部署生产」变成需要人工点头的显式动作
为什么要学
第 6 课的编排管「生产节奏」(每天几点跑),CI/CD 管「变更进入主干与生产的路径」。两者的区别:编排跑的是同样的代码在同样的环境按时间重复;CI/CD 跑的是每次代码或数据的变更。
ML 项目的变更面比后端宽:改了特征口径、换了一版数据、动了超参,都可能「本地好好的、合并全完蛋」。所以 CI 里除了常规 lint/单测,还要加三道 ML 特有关卡——数据 schema 校验、小样本训练冒烟、指标门禁。人话:CI 是门卫,代码、数据、模型三者谁要进主干,都得先过秤。
先建立图像
PR 提交
│
├─ job: quality ruff 静态检查(<1min)
├─ job: data-check 数据 schema 校验(<1min)
└─ job: smoke-train --smoke 冒烟训练 + 指标门禁 + 评论 PR(~3min)
│ 全绿才允许 merge
▼
merge 到 main
│
├─ job: image 构建 Docker 镜像 → 推送 registry(打 sha 标签)
└─ job: deploy-prod environment 审批 → kubectl apply
(连续训练 CT:生产侧由第 6 课的编排定时触发,产出经注册中心晋升)
跟着做
步骤 1:让训练脚本输出指标文件
给 src/train.py 的 main() 末尾加一行,把指标落成 JSON 供 CI 读取:
# src/train.py main() 中 log_metrics 之后追加
import json
json.dump({"f1": f1, "roc_auc": roc_auc},
open("metrics.json", "w"))
再加一个独立的数据校验脚本(CI 与生产管道共用):
# src/validate_data.py
import sys
import pandas as pd
REQUIRED = {"submit_id", "problem_id", "lang", "code_lines",
"rank", "verdict", "time_ms"}
def check(path="data/processed/judge_clean.parquet"):
df = pd.read_parquet(path)
missing = REQUIRED - set(df.columns)
assert not missing, f"缺字段: {missing}"
assert df["code_lines"].between(1, 10_000).all(), "code_lines 越界"
assert df["verdict"].isin(["AC", "TLE", "WA", "RE"]).all(), "非法 verdict"
tle = (df["verdict"] == "TLE").mean()
assert 0.01 < tle < 0.6, f"TLE 占比异常: {tle:.2f}" # 分布突变预警
print(f"data ok, rows={len(df)}, tle_rate={tle:.3f}")
if __name__ == "__main__":
check()
python src/validate_data.py
步骤 2:编写 GitHub Actions 工作流
# .github/workflows/ml-ci.yml
name: ml-ci
on:
pull_request:
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11", cache: pip }
- run: pip install ruff && ruff check src flows
data-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11", cache: pip }
- run: pip install -r requirements.txt
- run: python src/validate_data.py
smoke-train:
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11", cache: pip }
- run: pip install -r requirements.txt mlflow
- name: 冒烟训练(小样本,分钟级)
run: python src/train.py --smoke
- name: 指标门禁
run: |
python - <<'PY'
import json, sys
m = json.load(open("metrics.json"))
if m["roc_auc"] < 0.60:
sys.exit(f"门禁不通过: {m}")
print("gate passed:", m)
PY
- name: 把指标评论到 PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with: { path: metrics.json }
image:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: [quality, data-check, smoke-train]
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
deploy-prod:
if: github.ref == 'refs/heads/main'
needs: image
runs-on: ubuntu-latest
environment: production # 关键:需在仓库设置里批准
steps:
- uses: actions/checkout@v4
- run: kubectl set image deploy/timeout-api \
api=ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
步骤 3:推到 GitHub 看效果
pip freeze > requirements.txt
git add .github src && git commit -m "ci: 冒烟训练与指标门禁"
git push
打开仓库 Actions 页:PR 上能看到三个 job 的绿勾与指标评论;deploy-prod 停在等待 environment 审批的状态,维护者在 Settings → Environments 里点批才继续。
讲清楚要点
- CI for ML 的三道特有关卡:数据校验(schema/分布突变)、训练冒烟(小样本证明管道没断)、指标门禁(阈值不过就退出非零)。任何一环失败都挡住合并。
- 冒烟训练的价值是「管道保活」:它不追求模型质量,只证明「取数→特征→训练→评估」整条链路在干净环境能跑通。依赖缺失、路径写死、接口变更这类问题当场暴露,而不是三天后的编排任务里。
- 门禁阈值要写进代码而不是长在脑子里:
roc_auc < 0.60 就失败是可审计的;「我觉得不太行」不可审计。阈值可以随基线演进,但必须显式。 - CD 的对象是镜像而不是模型:CI/CD 发布「服务代码 + 推理环境」;模型文件走第 5 课的注册中心指针。两条发布流解耦,服务频繁发、模型按门禁节奏发。
- CT(持续训练)的触发源有三种:定时(编排器)、事件(新数据到位/漂移告警)、手动(业务要求)。本课程的组合是:编排器每天触发 + 漂移告警可触发,CI/CD 负责把训练代码的变更安全送达。
你可能会懵的地方
- CI runner 没有 GPU,深度学习项目怎么办? CI 只跑冒烟:小模型、小数据、CPU 几分钟内完成,验证链路而非效果。真正训练交给第 6 课的编排任务或第 9 课的云托管训练。也可以用 self-hosted runner 挂 GPU,但要评估并发与安全。
- 模型文件怎么在 job 之间传递?
用
actions/upload-artifact把模型或 metrics.json 存为工作流产物,下游 job 用download-artifact取。跨仓库/长期共享则走注册中心或对象存储,CI 只存临时产物。 - 坏数据进主干怎么防? 两道防线:CI 的 data-check 拦「代码仓库内的样例数据/校验逻辑」;生产侧第 10 课的漂移监控拦「线上真实数据突变」。CI 校验的是规则,监控校验的是现实。
- 为什么不把「训练 + 晋升 champion」也放进 CI? 完整训练慢且贵,放在 PR 流程会拖死开发;且模型发布应由门禁 + 审批控制节奏。经验法则:CI 验证能力(能不能跑、指标底线),CD 发布环境,CT 发布模型。
- secrets 怎么管理?
只用平台 secrets(
secrets.GITHUB_TOKEN、仓库级 secrets),注入环境变量;绝不把 token 写进代码、日志或镜像层。本地开发用 .env 且确认进了 .gitignore。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| CI 里跑全量训练几小时 | PR 反馈极慢,开发者绕过流程 | --smoke 小样本分钟级,全量训练归编排 |
| 门禁只有 lint 没有指标 | 模型劣化照样合并发布 | metrics.json + 阈值判断 + 失败退出 |
| 镜像统一打 latest 标签 | 无法回滚、无法定位版本 | 打 git sha 与语义化双标签 |
| 部署不需要审批,合并即上生产 | 一次误合并直接打穿生产 | environment 保护规则强制人工批准 |
| 训练脚本 secrets 写死在代码里 | 泄漏进 Git 历史无法挽回 | 平台 secrets 注入环境变量 |
卡点急救
- job 里
python: command not found:没用 setup-python 或版本装错;每个 job 都要显式uses: actions/setup-python@v5。 - pip 安装超时:加
cache: pip并提供 requirements.txt;依赖多时先跑缓存预热 job。 - kubectl 部署报 unauthorized:runner 没有集群凭据;用
azure/k8s-set-context或把 kubeconfig 存为 secret 注入,最小权限只授予目标 namespace。
面试常问
- ML 项目的 CI/CD 流水线你会怎么设计? PR 阶段:lint、单测、数据 schema 校验、冒烟训练、指标门禁、PR 评论指标;合并 main:构建镜像推 registry(sha 标签)+ environment 审批部署;模型本体走注册中心,由编排触发的 CT 或漂移事件经门禁晋升。关键原则:快(PR 反馈分钟级)、门禁代码化、服务与模型发布解耦。
- 什么是 CT(Continuous Training)?触发条件有哪些? 持续训练是 ML 特有的自动化:按条件自动重训并走门禁晋升。触发条件:定时节奏(每日/每周)、数据漂移告警、新标注数据到位、业务指标衰减、手动。核心是与第 5 课门禁、第 10 课监控闭环联动,而不是「到点就上线」。
- 模型发布和代码发布为什么解耦? 节奏不同(代码周级、模型按门禁触发)、风险不同(模型影响面靠监控评估)、载体不同(镜像 vs 注册中心指针)。解耦后各自回滚互不牵连:代码回滚动镜像 tag,模型回滚动 champion 指针。
自测三问
- ML 的 CI 比后端 CI 多出的三道关卡是什么?各拦截什么问题?
- 冒烟训练证明什么、不证明什么?
- 为什么模型文件不走 CI/CD 发布,而走注册中心?
小结一句话
CI/CD for ML = 后端门卫职责(lint/审批/镜像发布)+ ML 特有秤(数据校验、训练冒烟、指标门禁),服务发布与模型晋升解耦。
延伸
- GitHub Actions 官方文档——workflow 语法、secrets 与 environment 保护规则。
- 下一课:容器化部署——把推理服务打包成不可变镜像,再交给 K8s 弹性伸缩。