AI Infra实战11:模型部署Pipeline——CI/CD自动化

AI Infra实战11:模型部署Pipeline,CI/CD自动化

本篇目标

设计完整的模型发布Pipeline:从模型训练完成到线上服务更新的全自动化流程。

学完本篇你将掌握:

  • 模型CI/CD vs 代码CI/CD的核心差异
  • 完整的模型发布Pipeline设计
  • 模型质量门禁
  • 灰度发布和回滚策略
  • GitLab CI完整配置

模型CI/CD vs 代码CI/CD

维度 代码CI/CD 模型CI/CD
触发方式 代码push/MR合并 MLflow版本变更
构建产物 Docker镜像 模型文件(存S3)
测试方式 单元测试、集成测试 性能测试(延迟、吞吐量)
产物大小 几百MB 几GB-几十GB
发布速度 秒级 分钟级(下载模型+加载GPU)
回滚方式 改镜像tag 切MLflow版本+重新下载

代码CI/CD与模型CI/CD在触发、产物、发布速度上的对比


完整流程

复制代码
MLflow标记Staging → 触发Pipeline → 性能测试 → 灰度10% → 观察 → 全量 → 标记Production
                                      ↓ 不通过         ↓ 异常
                                   拒绝上线         自动回滚

验证到全量的四阶段主流程,以及不通过时的拒绝上线和自动回滚分支


Pipeline各阶段

阶段1:获取模型信息

从MLflow读取Staging版本的配置参数。

阶段2:质量门禁

检查项 通过条件
模型能加载 不报错
P99延迟 < 3000ms
吞吐量(20并发) > 10 req/s
显存占用 < 90%
输出合理 不是乱码

阶段3:灰度发布

部署Canary Pod(10%流量),观察5分钟。

阶段4:全量发布

验证通过后更新生产Deployment,MLflow标记Production。


性能测试详解:怎么测一个还没上线的模型

核心问题

模型还没部署到生产,怎么做压测?

答案:在CI中自动部署一个临时的测试实例,专门用来跑压测。测试完就删掉。

完整流程

复制代码
Pipeline的test阶段内部:

1. 自动创建测试Pod(独立namespace,不接生产流量)
        ↓
2. 等待测试Pod就绪(模型下载+加载,约2-3分钟)
        ↓
3. 对测试Pod跑压测(延迟、吞吐量)
        ↓
4. 判断结果:通过 → 继续下一阶段
             不通过 → Pipeline失败,拒绝上线
        ↓
5. 删掉测试Pod,释放GPU

CI中临时测试实例的生命周期,独立命名空间、不接生产流量、测完释放GPU

测试实例的设计要点

性能测试沿用微服务发布中"先测试环境验证再上生产"的思路,但有几个模型场景特有的约束需要在Pipeline里明确:

  • 测试GPU来源:使用集群预留的测试GPU,或在测试阶段临时扩一个GPU节点,测完释放。
  • 流量隔离:测试实例部署在独立namespace(如ai-test),生产Service的selector不会选中它,因此不承接线上流量。
  • 生命周期:由CI脚本负责创建和删除,测试结束后kubectl delete释放GPU,避免测试实例长期占卡。
  • 模型来源:initContainer从对象存储下载Staging版本的模型权重,与生产实例使用同一份产物。
  • 全程自动:创建、就绪等待、压测、判定、清理都写在CI脚本里,不需要人工介入。

GitLab CI配置

yaml 复制代码
stages:
  - validate
  - test
  - deploy-canary
  - verify
  - deploy-full
  - rollback

variables:
  MLFLOW_URI: "http://mlflow:5000"
  MODEL_NAME: "qwen2-inference-service"
  NAMESPACE: "ai-inference"
  DEPLOYMENT: "vllm-service"

# 获取Staging模型信息
validate:
  stage: validate
  script:
    - pip install mlflow -q
    - |
      python3 << 'EOF'
      from mlflow import MlflowClient
      import json, os
      client = MlflowClient(os.environ["MLFLOW_URI"])
      models = client.get_latest_versions(os.environ["MODEL_NAME"], stages=["Staging"])
      if not models:
          print("没有Staging版本")
          exit(1)
      model = models[0]
      run = client.get_run(model.run_id)
      config = {
          "version": model.version,
          "model_name": run.data.params.get("model_name", ""),
          "max_model_len": run.data.params.get("max_model_len", "4096"),
          "gpu_memory_utilization": run.data.params.get("gpu_memory_utilization", "0.9"),
      }
      with open("model_config.json", "w") as f:
          json.dump(config, f)
      print(f"Staging模型: v{model.version} - {config['model_name']}")
      EOF
  artifacts:
    paths:
      - model_config.json

# 性能测试(含临时测试实例的创建和销毁)
test:performance:
  stage: test
  needs: [validate]
  script:
    # ========== 第1步:部署临时测试实例 ==========
    - |
      echo "=== 部署测试实例 ==="
      MODEL_FILE=$(cat model_config.json | python3 -c "import json,sys;print(json.load(sys.stdin)['model_name'])")
      MAX_LEN=$(cat model_config.json | python3 -c "import json,sys;print(json.load(sys.stdin)['max_model_len'])")
      
      kubectl -n ai-test apply -f - << YAML
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: vllm-test
        namespace: ai-test
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: vllm-test
        template:
          metadata:
            labels:
              app: vllm-test
          spec:
            initContainers:
              - name: download-model
                image: amazon/aws-cli
                command: ["sh", "-c", "aws s3 cp s3://models/${MODEL_FILE}/ /models/ --recursive"]
                volumeMounts:
                  - name: model-vol
                    mountPath: /models
            containers:
              - name: vllm
                image: vllm/vllm-openai:latest
                command: ["vllm", "serve", "/models/${MODEL_FILE}", "--host=0.0.0.0", "--port=8000", "--max-model-len=${MAX_LEN}"]
                ports:
                  - containerPort: 8000
                resources:
                  limits:
                    nvidia.com/gpu: 1
                volumeMounts:
                  - name: model-vol
                    mountPath: /models
            volumes:
              - name: model-vol
                emptyDir: {}
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: vllm-test
        namespace: ai-test
      spec:
        selector:
          app: vllm-test
        ports:
          - port: 8000
      YAML

    # ========== 第2步:等待测试实例就绪 ==========
    - |
      echo "=== 等待测试实例就绪(最多5分钟)==="
      kubectl -n ai-test wait --for=condition=ready pod -l app=vllm-test --timeout=300s
      echo "测试实例就绪"

    # ========== 第3步:跑压测 ==========
    - |
      echo "=== 延迟测试(10个串行请求)==="
      total=0
      for i in $(seq 1 10); do
        start=$(date +%s%N)
        curl -s http://vllm-test.ai-test:8000/v1/chat/completions \
          -H "Content-Type: application/json" \
          -d '{"model":"test","messages":[{"role":"user","content":"hi"}],"max_tokens":20}' > /dev/null
        end=$(date +%s%N)
        ms=$(( (end - start) / 1000000 ))
        total=$((total + ms))
        echo "  请求$i: ${ms}ms"
      done
      avg=$((total / 10))
      echo "  平均延迟: ${avg}ms"

    # ========== 第4步:判断结果 ==========
    - |
      if [ $avg -gt 3000 ]; then
        echo "延迟超标: ${avg}ms > 3000ms,拒绝上线"
        kubectl -n ai-test delete deployment vllm-test
        kubectl -n ai-test delete service vllm-test
        exit 1
      fi
      echo "延迟达标: ${avg}ms < 3000ms"
      
      echo "=== 吞吐量测试(20并发)==="
      start=$(date +%s%N)
      for i in $(seq 1 20); do
        curl -s http://vllm-test.ai-test:8000/v1/chat/completions \
          -H "Content-Type: application/json" \
          -d '{"model":"test","messages":[{"role":"user","content":"hello"}],"max_tokens":20}' > /dev/null &
      done
      wait
      end=$(date +%s%N)
      elapsed=$(( (end - start) / 1000000 ))
      qps=$((20000 / elapsed))
      echo "  吞吐量: ${qps} req/s"
      
      if [ $qps -lt 10 ]; then
        echo "吞吐量不足: ${qps} < 10 req/s,拒绝上线"
        kubectl -n ai-test delete deployment vllm-test
        kubectl -n ai-test delete service vllm-test
        exit 1
      fi
      echo "吞吐量达标: ${qps} req/s"

    # ========== 第5步:清理测试实例 ==========
    - |
      echo "=== 清理测试实例 ==="
      kubectl -n ai-test delete deployment vllm-test
      kubectl -n ai-test delete service vllm-test
      echo "测试完成,GPU已释放"

# 灰度部署
deploy:canary:
  stage: deploy-canary
  needs: [test:performance]
  script:
    - |
      echo "=== 部署Canary(10%流量)==="
      kubectl -n $NAMESPACE apply -f canary-deployment.yaml
      echo "Canary部署完成"
  environment:
    name: production-canary

# 灰度验证
verify:canary:
  stage: verify
  needs: [deploy:canary]
  script:
    - |
      echo "=== 观察5分钟 ==="
      sleep 300
      # 查询canary的成功请求速率(vLLM暴露 vllm:request_success_total)
      # 失败率建议结合网关或应用层的HTTP状态码统计,指标名以实际vLLM版本的 /metrics 为准
      SUCCESS_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=sum(rate(vllm:request_success_total{track='canary'}[5m]))")
      echo "Canary成功请求速率: ${SUCCESS_RATE}"
      # 异常则回滚
      kubectl -n $NAMESPACE delete deployment ${DEPLOYMENT}-canary || true

# 全量部署(手动确认)
deploy:full:
  stage: deploy-full
  needs: [verify:canary]
  script:
    - |
      kubectl -n $NAMESPACE set env deployment/${DEPLOYMENT} \
        MODEL_VERSION=$(cat model_config.json | python3 -c "import json,sys;print(json.load(sys.stdin)['version'])")
      kubectl -n $NAMESPACE rollout status deployment/${DEPLOYMENT} --timeout=600s
      kubectl -n $NAMESPACE delete deployment ${DEPLOYMENT}-canary || true
      echo "全量部署完成"
  when: manual

# 回滚
rollback:
  stage: rollback
  script:
    - kubectl -n $NAMESPACE rollout undo deployment/${DEPLOYMENT}
    - echo "回滚完成"
  when: manual
  allow_failure: true

灰度部署详解:怎么让只有10%的流量到新模型

核心问题

灰度发布时,怎么控制只有一部分流量走新模型,大部分流量还走旧模型?

第07篇用KServe在真实GPU环境验证过一次灰度:给InferenceService设置canaryTrafficPercent,KServe自动把流量按10比90切到新旧两个Revision,并支持回滚和推广。那是平台已经封装好的能力。这一篇换个角度,看不依赖KServe时,怎么用更基础的K8s原语自己实现灰度,理解背后的流量分配原理。下面给两种方式。

方式一:Canary Deployment(最常用,不需要额外组件)

原理:利用K8s Service的负载均衡:多个Pod共享一个Service,流量按Pod数量比例自然分配。

Service按Pod数量分流,10个旧版本Pod加1个新版本Pod,新版本约拿9%流量

复制代码
生产环境现状:
  Deployment: vllm-service(10个Pod,跑v1模型)
  Service: vllm-svc → 负载均衡到这10个Pod

灰度操作:
  新增 Deployment: vllm-service-canary(1个Pod,跑v3模型)
  让这个canary Pod也被同一个Service选中

结果:
  Service后面有11个Pod(10个v1 + 1个v3)
  流量自然分配:v3拿到约 1/11 ≈ 9% 的流量

关键配置:

yaml 复制代码
# 生产Deployment(已有,10副本)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-service
spec:
  replicas: 10
  selector:
    matchLabels:
      app: vllm
  template:
    metadata:
      labels:
        app: vllm          # ← Service通过这个label选Pod
        version: v1        # ← 区分版本(用于监控区分)

---
# Canary Deployment(CI自动创建,1副本)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-service-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm
      version: v3
  template:
    metadata:
      labels:
        app: vllm          # ← 同样的label,也会被Service选中
        version: v3

---
# Service(不需要改,自动选中所有 app=vllm 的Pod)
apiVersion: v1
kind: Service
metadata:
  name: vllm-svc
spec:
  selector:
    app: vllm              # ← 选中v1的10个Pod + v3的1个Pod
  ports:
    - port: 8000

流量比例: 10个旧Pod + 1个新Pod = 新模型拿到约9%流量。

方式二:Istio精确流量控制(需要Service Mesh)

如果集群装了Istio,可以精确控制任意百分比:

yaml 复制代码
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: vllm-vs
spec:
  hosts:
    - vllm-svc
  http:
    - route:
        - destination:
            host: vllm-svc
            subset: stable
          weight: 90            # 90%走旧模型
        - destination:
            host: vllm-svc
            subset: canary
          weight: 10            # 10%走新模型

两种方式对比

维度 Canary Deployment Istio
精确度 按Pod数比例(粗略) 任意百分比(精确)
依赖 只需要K8s 需要安装Istio
复杂度 低(CI里kubectl apply就行)
适合 大多数场景 需要精确控制时

建议: 没有Istio就用Canary Deployment,够用了。


灰度期间怎么观察

部署canary后观察5-10分钟,对比新旧模型的指标:

指标 v1(stable) v3(canary) 判定
P99延迟 2.1s 1.8s 更好
错误率 0.1% 0.2% 在可接受范围
GPU显存 12GB 6GB 更省

用Prometheus区分新旧版本的指标(通过version label):

vLLM暴露的是成功请求计数 vllm:request_success_total。给新旧版本的Pod打上不同的 version label 后,可以分别观察两版的请求速率;失败率建议结合网关或应用层的 HTTP 状态码统计。具体指标名以所用 vLLM 版本的 /metrics 输出为准。

promql 复制代码
# canary(v3)的成功请求速率
sum(rate(vllm:request_success_total{version="v3"}[5m]))

# stable(v1)的成功请求速率
sum(rate(vllm:request_success_total{version="v1"}[5m]))

灰度失败怎么回滚

bash 复制代码
# 直接删掉canary Deployment
kubectl delete deployment vllm-service-canary

canary Pod没了 → Service只剩v1的Pod → 流量自动100%回到旧模型。不需要改Service配置。

灰度成功怎么全量

bash 复制代码
# 1. 更新生产Deployment的模型版本(触发滚动更新)
kubectl set env deployment/vllm-service MODEL_VERSION=v3

# 2. 等待所有旧Pod滚动更新成v3
kubectl rollout status deployment/vllm-service

# 3. 删掉canary(生产Deployment已经全部是v3了)
kubectl delete deployment vllm-service-canary

灰度实践中的几个关键点

  • 比例精度:Service按Pod数量分流是粗粒度的,10个旧Pod加1个canary约等于9%,多数场景够用。需要精确到任意百分比时用Istio的weight,或按第07篇KServe的canaryTrafficPercent控制。
  • 调整比例:想走5%到20%再到100%的阶梯,可以调canary的replicas数量,或调Istio/KServe的权重。
  • 对存量的影响:canary是新增的Deployment,不改动原有Pod,灰度期间旧模型不受影响。
  • 用户感知:如果新旧模型回答风格差异明显,被分到canary的少量用户可能察觉,需要在验证阶段一并评估输出质量,而不只看延迟和错误率。
  • 观察时长:看基本指标5到10分钟即可,核心服务建议观察30分钟到1小时,覆盖一个较完整的流量波峰。

质量门禁设计

级别 检查项 不通过的后果
P0 模型能加载 阻断,不上线
P0 请求不报错 阻断
P1 P99延迟 < SLO 阻断
P1 吞吐量 > 基线80% 阻断
P2 显存 < 90% 告警但不阻断

实践边界

本篇是模型部署Pipeline的设计方案,整理自MLflow、GitLab CI和K8s的常规用法,不新增云资源,成本为0。需要说明验证情况:

  • 其中的灰度、回滚能力,第07篇已用KServe在真实GPU环境验证过(canaryTrafficPercent按10比90切分、回滚、推广到100%)。
  • 本篇的GitLab CI配置、临时测试实例和质量门禁,是可落地的设计模板,尚未在完整CI环境端到端跑通,落地时需结合具体的MLflow地址、对象存储和GPU配额调整参数。
  • vLLM的指标名以所用版本的 /metrics 输出为准,不同版本可能有差异。

不把设计模板写成已上线的生产流水线,是为了让读者清楚哪些是已验证的、哪些是待落地的。


小结

  1. 模型CI/CD多了质量门禁:延迟和吞吐量必须达标
  2. 四阶段流程:验证→测试→灰度→全量
  3. 灰度+自动回滚:异常时自动切回旧版本
  4. 底层仍是熟悉的工具链:GitLab CI加K8s滚动更新,核心增量是模型产物管理和上线前的性能门禁

下一篇预告

AI Infra实战12:GPU成本优化实践


参考链接

相关推荐
HZZD_HZZD1 小时前
商业综合体分户计量与能源计费:业态差异、公区分摊与转供电合规
大数据·人工智能·能源
一线数智1 小时前
制造业的下一场竞争,不在减人,而在重新定义人与机器的关系
人工智能
网络研究院1 小时前
告别野蛮生长!苹果 Apple Music 重拳监管 AI 音乐
人工智能·媒体·音乐·标注·歌曲·执行·监管
xlq223221 小时前
Ai大模型接入sdk day5
人工智能
Austin_YB1 小时前
如何用 AI全程托管开发(Agent + SAP MCP 工具链 + 自定义技能)把功能说明书变成系统功能
人工智能
python零基础入门小白1 小时前
LangGraph智能体实战:如何用Langfuse构建AI运行时全链路可观测系统?
人工智能·学习·ai·chatgpt·程序员·大模型·智能体
xiancai_xianyu1 小时前
想把数据模型一次建完美再上AI?会一直卡在建模里
开发语言·人工智能·php·数据模型·语义层·本体建模·企业知识网络
2601_962202981 小时前
首衡集配用万象,企业竞争力不断增强
大数据·人工智能
小小测试开发1 小时前
LLM评测:LLM-as-Judge 想当回归门禁,先过偏差校准和双层评分这两关
人工智能·数据挖掘·回归