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 还在显存里吗?问题驱动学习,比知识点驱动学习更有效。
系列目录:
- GPU 虚拟化与 MIG:一块 A100 如何被多个 Pod 瓜分
- RDMA 与 GPUDirect:绕过 CPU 和内核的数据搬运术
- InfiniBand 实战:从子网管理器到无损网络
- K8s 设备插件与 GPU Operator:GPU 资源的声明式管理
- NVIDIA Network Operator:多网卡 SR-IOV 与 NIC 配置自动化
- 分布式训练:torchrun、NCCL 与 GPU 拓扑感知调度
- vLLM 与 Triton:LLM 推理服务的生产化之路
- MLOps 流水线:从训练到推理的 CI/CD(本篇)