CI/CD for ML
CML、Jenkins 与 Ansible IaC。
30 分钟
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 }}