MLOps 流水线:从训练到推理的 CI/CD

AI 加速器系列 · 第 8 篇

软件部署错了回滚一个二进制就行。模型部署错了------精度下降 3%,错误率翻倍------你不知道问题出在数据、训练代码、还是模型本身。MLOps 管的不是一个 artifact,是三个。


1. MLOps 与传统 DevOps 的本质区别

传统的 DevOps 流水线是线性的。你有一个代码仓库,CI 构建二进制/容器,CD 推送到生产环境。回滚就是把上一个版本的二进制重新部署上去。整个流程里只有一个变量:代码。

MLOps 有三个变量:代码、数据、模型参数。任何一个变了,产出物就变了。更麻烦的是,这三个变量不是独立的------数据分布变了,同样的代码跑出来的模型就不对;超参调了,同样的数据出来的结果也不同。

对比一下两条流水线的拓扑:

rust 复制代码
DevOps Pipeline (线性)
  Code  --->  Build  --->  Test  --->  Deploy  --->  Monitor
                      |                                    |
                      +---------  回滚二进制  <------------+

MLOps Pipeline (环路)
  Data  ---+                        +-- Canary (10% Traffic)
           +-->  Train  -->  Validate  -->  Package  -->  Deploy  --+
  Code  ---+                        |   Model Registry              |
                                    +-- A/B Test  ------------------+
                                                                     |
  Retrain  <--  Trigger  <--  Monitor (Drift + Degradation)  <------+

Google 把 MLOps 成熟度分成三个等级:

等级 特征 典型场景
Level 0 手动训练脚本,模型文件手动拷贝到服务器 团队里唯一会训练模型的那位博士
Level 1 训练流水线自动化,CT (Continuous Training) 有 CI/CD 但数据/模型没有版本化
Level 2 全链路 CI/CD + CT,数据和模型版本化,自动回滚 Google / NVIDIA 内部

绝大多数团队卡在 Level 0 和 Level 1 之间------有一个勉强能跑的训练脚本,一个 Dockerfile 把模型打包进去,手动 kubectl apply 更新 serving。这在模型一个月更新一次时没问题。当模型每天都要重新训练,每周要推一个新版本时,手动操作就是灾难。


2. 流水线六阶段拆解

Stage 1: Data Versioning(数据版本化)

训练数据每天都在变------新的用户行为、新的标注、新的数据源。问题是:上周的那个模型是用什么数据训的?

答案不能是"大概是上周的那个 CSV"。数据版本化要回答三个问题:

  • 某个模型用了哪个版本的数据集?
  • 当前的数据集和上一版比差了什么?
  • 能不能复现六个月前的一次训练?

DVC(Data Version Control)的思路是把数据的 hash 和元信息存在 Git 里,实际数据存在 S3/MinIO 里:

bash 复制代码
Git 仓库                  S3 / MinIO
┌──────────────────┐     ┌──────────────────┐
│ .git/            │     │                  │
│ data/train.csv   │     │  s3://bucket/    │
│   → a1b2c3.dvc  │     │    a1b2c3.train  │
│ data/labels.csv  │     │    d4e5f6.labels │
│   → d4e5f6.dvc  │     │                  │
└──────────────────┘     └──────────────────┘

LakeFS 更进一步,给 S3 加上 Git 语义------你可以给数据集打 tag、切 branch、做 diff:

yaml 复制代码
# lakefs.yaml ------ 数据集分支策略
branches:
  - name: production-stable
    description: 当前线上模型使用的训练数据
  - name: experiment/user-profile-v2
    description: 增加了用户画像特征的数据实验

实际操作中,每次训练前先拉取指定版本的数据集,训练完成后把 input_data_hash 记录到模型元信息里。这样任何模型都可以追溯到它的数据血统。

Stage 2: Experiment Tracking(实验追踪)

当你跑了 50 组超参组合,三周后需要找出 F1 最高的那组时,你是在翻聊天记录还是 grep 日志文件?

MLflow Tracking 解决了这个问题。几行代码把所有关键信息记录下来:

python 复制代码
import mlflow

with mlflow.start_run():
    # 元信息
    mlflow.set_tag("model_type", "resnet50")
    mlflow.log_param("learning_rate", 0.001)
    mlflow.log_param("batch_size", 64)

    # 训练循环
    for epoch in range(num_epochs):
        loss, accuracy = train_one_epoch(model, train_loader)
        mlflow.log_metric("train_loss", loss, step=epoch)
        mlflow.log_metric("train_accuracy", accuracy, step=epoch)

        val_loss, val_acc = validate(model, val_loader)
        mlflow.log_metric("val_loss", val_loss, step=epoch)
        mlflow.log_metric("val_accuracy", val_acc, step=epoch)

    # 保存模型
    mlflow.pytorch.log_model(model, "model")

    # 记录数据版本(来自 Stage 1)
    mlflow.log_param("data_version", "d4e5f6")
    mlflow.log_param("git_commit", "a1b2c3d")

Weights & Biases 是 MLflow 的商业替代方案,UI 更友好,但核心思路一样。不管用哪个工具,一个实验记录至少要包含五个维度:

维度 内容 为什么重要
Code Version git commit hash 这周的 bug 修了,上周的还在
Data Version DVC hash / LakeFS tag 数据变了结果就变了
Hyperparameters lr, bs, optimizer, scheduler 定位最优配置
Metrics loss, accuracy, throughput 客观对比的依据
Model Artifact s3://model-registry/run-xxx 可以随时回滚到这个版本

Stage 3: Training Pipeline(训练流水线)

单个 GPU 跑 ResNet 很快。但当你需要在 8 个 A100 上训练 LLaMA 级别的模型,或者同时跑 20 组消融实验时,训练本身变成了一个资源调度问题。

Kubernetes 上的分布式训练通常用 Kubeflow Training Operator 或 Volcano 来调度:

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: bert-finetune-job
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      restartPolicy: OnFailure
      template:
        spec:
          containers:
            - name: pytorch
              image: registry.example.com/train/bert-finetune:v2.3
              command:
                - torchrun
                - --nnodes=4
                - --nproc_per_node=8
                - train.py
                - --data-version=$(DATA_VERSION)
                - --output=/mnt/model-registry/
              resources:
                limits:
                  nvidia.com/gpu: 8
              env:
                - name: MLFLOW_TRACKING_URI
                  value: http://mlflow-server:5000
                - name: DATA_VERSION
                  valueFrom:
                    configMapKeyRef:
                      name: dataset-config
                      key: current-version
    Worker:
      replicas: 3             # 共 4 节点 × 8 GPU = 32 GPU
      restartPolicy: OnFailure
      template:
        spec:
          containers:
            - name: pytorch
              image: registry.example.com/train/bert-finetune:v2.3
              resources:
                limits:
                  nvidia.com/gpu: 8

超参搜索用 Katib 自动化:

yaml 复制代码
apiVersion: kubeflow.org/v1beta1
kind: Experiment
metadata:
  name: lr-batch-size-search
spec:
  objective:
    type: maximize
    goal: 0.95
    objectiveMetricName: val_accuracy
  algorithm:
    algorithmName: bayesian-optimization  # 贝叶斯优化, 比 grid search 高效得多
  parameters:
    - name: learning_rate
      parameterType: double
      feasibleSpace:
        min: "0.0001"
        max: "0.1"
    - name: batch_size
      parameterType: int
      feasibleSpace:
        min: "16"
        max: "256"
  trialTemplate:
    primaryContainerName: training-container
    trialParameters:
      - name: learningRate
        reference: learning_rate
      - name: batchSize
        reference: batch_size
    trialSpec:
      apiVersion: batch/v1
      kind: Job
      spec:
        template:
          spec:
            containers:
              - name: training-container
                image: registry.example.com/train/search:latest
                command:
                  - python
                  - train.py
                  - --lr=${trialParameters.learningRate}
                  - --batch-size=${trialParameters.batchSize}
            restartPolicy: Never

训练完成后的触发链路:

objectivec 复制代码
Train Job 完成
  → eval 脚本自动跑验证集
  → accuracy > threshold?
    YES → mlflow models register → 自动创建 Deployment 变更 PR
    NO  → 结果归档,不推进到下一阶段

Stage 4: Model Packaging & Serving(模型打包与服务化)

训练产出的是 PyTorch 的 .pt / .pth 文件。但直接用它做线上推理性能很差。需要一个优化链路:

scss 复制代码
PyTorch (research)
  │  torch.onnx.export()
  ▼
ONNX (intermediate representation)
  │  trtexec --onnx=model.onnx --saveEngine=model.plan
  ▼
TensorRT Engine (optimized for specific GPU)
  │  FP16 / INT8 quantization
  ▼
Production serving container

容器镜像的层次:

scss 复制代码
┌────────────────────────────────────┐
│  Model-specific Config             │  ← batch_size, max_seq_len
│  (ConfigMap, hot-reloadable)       │
├────────────────────────────────────┤
│  TensorRT Engine                   │  ← 30MB ~ 20GB, 取决于是 BERT 还是 LLaMA
│  (体积大,单独分区存储)              │
├────────────────────────────────────┤
│  Serving Framework (vLLM / Triton) │  ← 复用同一份,version bump only
├────────────────────────────────────┤
│  CUDA + cuDNN Runtime              │  ← 几乎不变
├────────────────────────────────────┤
│  Base Image (nvidia/cuda:12.x)     │  ← 极少更新
└────────────────────────────────────┘

Serving 框架选择:

场景 推荐 原因
通用模型推理 NVIDIA Triton Inference Server 支持多种 backend,动态批处理,模型 ensemble
大语言模型专用 vLLM PagedAttention,连续批处理,吞吐量远超普通方案
极低延迟(<1ms) TensorRT-LLM C++ runtime 去掉 Python 层开销

模型注册为 immuatable artifact:

bash 复制代码
# curl 方式注册到 MLflow Registry
mlflow models register \
  --model-name sentiment-classifier \
  --model-uri s3://model-registry/run-a1b2c3d/model \
  --tags git_commit=a1b2c3d \
  --tags data_version=d4e5f6 \
  --tags eval_accuracy=0.973

# 标记为 staging,等待部署验证
mlflow models transition \
  --model-name sentiment-classifier \
  --version 42 \
  --stage staging

Stage 5: Progressive Deployment(渐进式部署)

一次切全部流量到新模型是赌博。三种渐进策略:

Canary Rollout(金丝雀发布)

matlab 复制代码
Istio VirtualService 路由规则

  HTTP Request
      │
      ├── weight: 90% ──→ k8s-svc:model-v41  (stable)
      │
      └── weight: 10% ──→ k8s-svc:model-v42  (canary)

  观察 30min:p99 latency, error_rate, 预测分布
    ├── 指标正常 → 50% → 100%
    └── 指标异常 → 撤销 canary,切回 100% v41
yaml 复制代码
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-serving-route
spec:
  hosts:
    - inference-api
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"          # 内部测试流量强制走 canary
      route:
        - destination:
            host: model-v42-canary
            port:
              number: 8000
    - route:
        - destination:
            host: model-v41-stable
            port:
              number: 8000
          weight: 90
        - destination:
            host: model-v42-canary
            port:
              number: 8000
          weight: 10

A/B Testing(A/B 对比)

与 Canary 的关键区别:A/B 对比的是业务指标,不是运维指标。

css 复制代码
User-Agent hash % 2 == 0  →  Model A  (v41)
User-Agent hash % 2 == 1  →  Model B  (v42)

对比的指标:
  ┌──────────────────┬─────────┬─────────┐
  │ Metric           │ Model A │ Model B │
  ├──────────────────┼─────────┼─────────┤
  │ CTR              │  12.3%  │  12.1%  │
  │ Conversion Rate  │   4.7%  │   5.2%  │  ← B 显著优于 A!
  │ Avg Session Time │   182s  │   190s  │
  └──────────────────┴─────────┴─────────┘

Shadow Deployment(影子部署)

100% 真实流量镜像一份给新模型,新模型的预测结果不返回给用户,只用来收集评估数据。这是风险最低的方式,但成本最高------两倍的 GPU 算力。

yaml 复制代码
# Istio 流量镜像
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-shadow
spec:
  hosts:
    - inference-api
  http:
    - route:
        - destination:
            host: model-v41-stable
          weight: 100
      mirror:
        host: model-v42-shadow       # 镜像流量,不返回结果
      mirrorPercentage:
        value: 100.0

Rollback(回滚)

回滚的关键不是"怎么回",而是"能不能 30 秒内完成"。如果旧模型的 TensorRT Engine 不在 GPU 显存里,加载可能需要几分钟。所以 P0 服务需要 keep warm:

yaml 复制代码
# Deployment 中保留旧版本的 standby 副本
apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-v41-standby
spec:
  replicas: 1                  # 至少一个 Pod 保持 GPU 显存 warm
  template:
    spec:
      containers:
        - name: vllm-server
          image: registry.example.com/vllm:v0.4.2
          args: ["--model", "/models/v41/", "--gpu-memory-utilization", "0.90"]
          resources:
            limits:
              nvidia.com/gpu: 1

回滚操作就是 Istio 权重切回 100% stable。如果你的 Istio 控制面正常,30 秒内完成。

Stage 6: Continuous Monitoring & Auto-Retraining(持续监控与自动重训)

部署完不等于结束。模型的精度是在时间轴上衰减的------不是因为代码变了,而是因为世界变了。这叫 Model Drift

监控两个维度:

java 复制代码
Data Drift (输入分布漂移)
  训练时用户平均年龄:28.5
  当前请求用户平均年龄:34.2   ← 分布发生了显著变化

Model Performance Degradation (模型表现退化)
  上线时 accuracy: 0.973
  一个月后 accuracy: 0.941     ← 阈值之下,触发告警

监控架构:

scss 复制代码
                         ┌──────────────────────┐
  Prediction Requests ──→│  Feature Store /      │
                         │  Streaming (Kafka)    │
                         └──────┬───────────────┘
                                │
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                  ▼
     ┌────────────┐   ┌───────────────┐   ┌──────────────┐
     │ Data Drift  │   │ Performance   │   │ Latency/     │
     │ Detector    │   │ Monitor       │   │ Throughput   │
     │ (Evidently) │   │ (Prom +       │   │ (Prometheus) │
     │             │   │  Grafana)     │   │              │
     └──────┬──────┘   └──────┬────────┘   └──────────────┘
            │                 │
            ▼                 ▼
     ┌──────────────────────────────────┐
     │  Threshold Breach?               │
     │  accuracy < 0.95  OR  KL-        │
     │  divergence > 0.3                │
     └────────────┬─────────────────────┘
                  │ YES
                  ▼
     ┌──────────────────────────────────┐
     │  Auto-Retraining Trigger         │
     │  → 启动 PytorchJob               │
     │  → 使用最新的数据 + 同样的代码      │
     │  → 训练完成 → eval gate          │
     │  → accuracy > 0.97?              │
     │    YES → 推进到 Stage 4          │
     │    NO  → 告警通知人工介入          │
     └──────────────────────────────────┘

自动重训的 eval gate 是必要保护------新训练出来的模型不一定比旧的好。不能直接把自动重训的结果推向生产。


3. GitOps + MLOps = 声明式 ML 基础设施

GitOps 和 MLOps 各自管不同层次的东西。把它们混在一个仓库里是反模式------一个仓库里既有一周不变一次的 GPU Operator 配置,也有一天变十次的实验参数,必然彼此阻塞。

分层策略:

java 复制代码
┌────────────────────────────────────────────┐
│  Infrastructure Repo (GitOps via ArgoCD)   │
│  - GPU Operator                             │  ← 变更频率:月
│  - Prometheus Stack                         │
│  - Istio Service Mesh                       │
│  - vLLM Serving Deployment (基线配置)        │
│  - Namespace / RBAC / NetworkPolicy        │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│  ML Platform Repo (ML Pipeline Orchestrator)│
│  - Training Pipeline Definitions            │  ← 变更频率:天
│  - Experiment Configurations               │
│  - Model Serving Traffic Rules             │
│  - Monitoring Thresholds                   │
│  - Data Version Pointers                   │
└────────────────────────────────────────────┘

ArgoCD 管理基础设施层:

yaml 复制代码
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gpu-infra
  namespace: argocd
spec:
  project: infrastructure
  source:
    repoURL: https://github.com/example/infra-gitops.git
    targetRevision: main
    path: overlays/production/gpu-cluster
  destination:
    server: https://kubernetes.default.svc
    namespace: gpu-operator
  syncPolicy:
    automated:
      prune: true
      selfHeal: true       # GPU Operator 配置漂移自动修复
    syncOptions:
      - CreateNamespace=true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vllm-serving-base
  namespace: argocd
spec:
  project: infrastructure
  source:
    repoURL: https://github.com/example/infra-gitops.git
    targetRevision: main
    path: apps/vllm-serving/base
    helm:
      valueFiles:
        - values-production.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: model-serving
  syncPolicy:
    automated:
      prune: false
      selfHeal: false      # Serving 配置不要自动修复------变更要走审批

而对于模型层的持续部署,事件驱动的编排更合适:

erlang 复制代码
Model Registry 事件
  │  "model-v42 status: validated"
  ▼
Argo Events / Sensor
  │  触发 CD Pipeline
  ▼
Argo Workflows
  │  1. 渲染 Istio VirtualService (新权重)
  │  2. kubectl apply
  │  3. 等待 canary 观察期结束
  │  4. 全部成功 → 100% 流量切换
  │  5. 归档旧模型(30 天保留期后删除)
  ▼
Slack/Webhook 通知

4. 完整的端到端流程

把六个阶段串在一起,从数据变更到模型上线全自动化:

ini 复制代码
  08:00 ── 新用户数据导入 S3, DVC tag: data-prod-20260809
  08:15 ── LakeFS 合并到 production 分支
  08:30 ── CronTrigger 启动 Training Pipeline
           │
           ├── Katib 并行跑 4 组超参数试验 (2 GPU each = 8 GPU)
           │   best trial: lr=0.003, bs=128, val_acc=0.974
           │
  10:45 ── 训练完成 → mlflow register → staging
  10:50 ── Accuracy Gate: 0.974 > 0.95 ✓
  10:51 ── 自动生成 Serving Deployment PR
  10:52 ── CI: 构建 serving 镜像 + TensorRT 优化
  11:05 ── Canary: 10% 流量到 model-v43
  11:35 ── 30min 观察期: p99=42ms (baseline=45ms), error_rate=0.01%
  11:36 ── Canary 通过 → 50% → 100% 全量切换
  11:37 ── Slack: "model-v43 已全量上线, val_acc=0.974, 性能优于基线"

这不是科幻。NVIDIA 内部和头部云厂商的 ML 平台已经跑着这样的流水线。差距不在于有没有这个能力,而在于能不能把六个阶段的工具链咬合在一起。


5. 关键选型速查

环节 开源方案 托管/商业方案
数据版本化 DVC + MinIO, LakeFS Databricks Delta Lake, Pachyderm
实验追踪 MLflow Tracking Weights & Biases, Neptune.ai
训练编排 Kubeflow Training Operator, Volcano Run:ai, Anyscale
超参搜索 Katib Optuna + Weights & Biases Sweeps
模型注册 MLflow Model Registry Hugging Face Hub, NVAIE
模型优化 ONNX Runtime + TensorRT (open source部分) NVIDIA TensorRT Enterprise
Serving vLLM, Triton Inference Server BentoML, Seldon Core
流量管理 Istio 各云厂商 Service Mesh
监控告警 Evidently AI + Prometheus + Grafana WhyLabs, Arize AI
管线编排 Argo Workflows + Events Kubeflow Pipelines, Flyte

一句话总结

MLOps 的本质不是"为 ML 团队配一套 CI/CD",而是让数据、模型、代码三个 artifact 的变更链路可追踪、可复现、可回滚------当一个变量变动时,系统自动验证另外两个变量是否仍然兼容。

系列完结


AI 加速器系列全部 8 篇:从 GPU 虚拟化到分布式训练,从 L4/L7 负载均衡到 MLOps CI/CD ------ 这些是 NVIDIA NCX 工程师需要横跨的技术光谱。每一个知识点的背后都有一组假设:K8s 调度器知道 GPU 拓扑吗?VPC 安全组对 InfiniBand 流量有影响吗?Canary rollout 时旧模型的 TensorRT Engine 还在显存里吗?问题驱动学习,比知识点驱动学习更有效。

系列目录:

  1. GPU 虚拟化与 MIG:一块 A100 如何被多个 Pod 瓜分
  2. RDMA 与 GPUDirect:绕过 CPU 和内核的数据搬运术
  3. InfiniBand 实战:从子网管理器到无损网络
  4. K8s 设备插件与 GPU Operator:GPU 资源的声明式管理
  5. NVIDIA Network Operator:多网卡 SR-IOV 与 NIC 配置自动化
  6. 分布式训练:torchrun、NCCL 与 GPU 拓扑感知调度
  7. vLLM 与 Triton:LLM 推理服务的生产化之路
  8. MLOps 流水线:从训练到推理的 CI/CD(本篇)
相关推荐
今日无bug1 小时前
Tool Use 工具调用 —— 让LLM缸中大脑突破物理限制的3个阶段
node.js·llm·agent
禁止摆烂_才浅1 小时前
Axios 高频面试题
前端·面试·axios
何时梦醒1 小时前
TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof
前端·面试·typescript
烬羽1 小时前
CSS 三栏布局:`margin-left: -100%` 凭什么把侧栏「拽」回上一行?彻底搞懂负 margin
css·面试·前端框架
缓冲中请稍后1 小时前
前端HTTP请求完全指南:从基础到LLM接口调用实战
前端·面试
shepherd1111 小时前
国产模型越来越强了:DeepSeek V4、Kimi K3 与 GLM 最新进展
人工智能·llm·ai编程
禁止摆烂_才浅1 小时前
JavaScript WebAPI(进阶)高频面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
ES6+ 高频面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
React Hooks 高频面试题
前端·react.js·面试