AI 模型云原生部署:从 GPU 调度到推理服务弹性伸缩的实战路径

一、GPU 资源浪费过半——AI 推理上云的第一道坎

AI 模型部署到 K8s,最扎心的现实:GPU 利用率不到 40%。模型推理服务白天高峰需要 4 张 A100,凌晨低谷只需要 1 张,但 GPU 节点按峰值配置,剩下的全在空转。一张 A100 月租过万,浪费的就是真金白银。

更棘手的问题:

  • GPU 不支持分时复用:一个 Pod 占了一张卡,其他 Pod 就用不了,不像 CPU 可以按毫核切分
  • 模型加载慢:大模型动辄几十 GB,冷启动一次要 30 秒以上,HPA 扩容根本来不及
  • 显存碎片化:多个小模型各占一张卡的一部分,剩余显存又不够跑大模型
  • 驱动版本耦合:NVIDIA 驱动、CUDA 版本、容器运行时三者必须对齐,升级一个全得动

别整虚的,直接上方案。

二、云原生 AI 推理的架构与调度机制

云原生 AI 推理的核心矛盾:GPU 是刚性资源,推理负载是弹性需求。解决方案的思路——把刚性资源池化,把弹性需求做分层调度。

graph TB
    subgraph "推理服务层"
        A[API Gateway] --> B[推理调度器]
        B --> C[模型 A: vLLM Runtime]
        B --> D[模型 B: Triton Server]
        B --> E[模型 C: TGI Runtime]
    end
    subgraph "GPU 资源池"
        F[GPU 节点 1: A100 x4]
        G[GPU 节点 2: A100 x4]
        H[GPU 节点 3: A10 x2]
    end
    subgraph "调度策略"
        I[时间分片 - GPU Time-Slicing]
        J[MPS - 多进程服务]
        K[动态调度 - DRA]
    end
    C --> F
    D --> G
    E --> H
    I --> F
    J --> G
    K --> H

关键机制拆解:

机制 原理 适用场景
GPU Time-Slicing 时间片轮转,多 Pod 共享一张 GPU 低延迟不敏感的批量推理
NVIDIA MPS 多进程共享 GPU 上下文,减少切换开销 同构模型多实例
DRA (Dynamic Resource Allocation) K8s 1.26+ 原生 GPU 分配 API 需要精确 GPU 拓扑感知
GPU 共享调度器 自定义 Scheduler Extender,按显存比例分配 多小模型共享 GPU

三、生产级 AI 推理服务部署方案

3.1 基于 vLLM 的推理服务 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
  namespace: ai-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      # 调度到 GPU 节点
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: nvidia.com/gpu.product
                    operator: In
                    values:
                      - A100-SXM4-80GB
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.6.1
          args:
            - --model
            - /models/qwen2-72b
            - --tensor-parallel-size
            - "2"          # 2 张 GPU 并行推理
            - --max-model-len
            - "8192"       # 最大序列长度,控制显存占用
            - --gpu-memory-utilization
            - "0.90"       # 显存利用率上限 90%,预留防 OOM
            - --served-model-name
            - qwen2-72b
          ports:
            - containerPort: 8000
          resources:
            requests:
              nvidia.com/gpu: "2"  # 请求 2 张 GPU
              cpu: "8"
              memory: "32Gi"
            limits:
              nvidia.com/gpu: "2"  # GPU 不可超限
              cpu: "8"
              memory: "32Gi"
          # 存活探针:检测推理服务是否响应
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120  # 模型加载需要时间
            periodSeconds: 30
            failureThreshold: 3
          # 就绪探针:模型加载完成后才接收流量
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 10
          volumeMounts:
            - name: model-storage
              mountPath: /models
      volumes:
        # 模型文件用 PVC 挂载,避免每次拉取
        - name: model-storage
          persistentVolumeClaim:
            claimName: llm-model-pvc

3.2 GPU Time-Slicing 配置

在 GPU 节点上配置时间片共享,让多 Pod 复用同一张卡:

# ConfigMap: GPU 时间片配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: gpu-time-slicing
  namespace: gpu-operator
data:
  config.yaml: |
    version: v1
    sharing:
      timeSlicing:
        renameByDefault: false
        failRequestsGreaterThanOne: true
        resources:
          - name: nvidia.com/gpu
            replicas: 4  # 一张物理 GPU 虚拟为 4 个时间片

3.3 推理服务弹性伸缩策略

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
  namespace: ai-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicas: 1
  maxReplicas: 8
  metrics:
    # 自定义指标:推理队列深度
    - type: Pods
      pods:
        metric:
          name: inference_queue_depth
        target:
          type: AverageValue
          averageValue: "5"  # 平均队列深度超过 5 触发扩容
    # 自定义指标:GPU 显存利用率
    - type: Pods
      pods:
        metric:
          name: gpu_memory_utilization
        target:
          type: AverageValue
          averageValue: "0.85"  # 显存利用率超过 85% 触发扩容
  behavior:
    scaleUp:
      # 扩容策略:每次最多扩 2 个 Pod
      policies:
        - type: Pods
          value: 2
          periodSeconds: 120
    scaleDown:
      stabilizationWindowSeconds: 600  # 10 分钟稳定窗口

3.4 模型预热与缓存——解决冷启动

import asyncio
import httpx
from kubernetes import client, config
from kubernetes.client import V1Pod

class ModelWarmer:
    """推理模型预热器:在 Pod 就绪后发送预热请求,加载模型到 GPU"""

    def __init__(self, namespace: str = "ai-serving"):
        config.load_incluster_config()
        self.k8s_api = client.CoreV1Api()
        self.namespace = namespace

    async def warm_up_pod(self, pod_ip: str, model_name: str) -> bool:
        """向指定 Pod 发送预热请求,触发模型加载"""
        url = f"http://{pod_ip}:8000/v1/completions"
        # 构造最小化预热请求,仅激活模型加载
        payload = {
            "model": model_name,
            "prompt": "warmup",
            "max_tokens": 1,
            "temperature": 0
        }
        try:
            async with httpx.AsyncClient(timeout=120.0) as client:
                resp = await client.post(url, json=payload)
                return resp.status_code == 200
        except httpx.TimeoutException:
            # 预热超时,记录日志但不阻塞
            print(f"Warmup timeout for pod {pod_ip}")
            return False

    async def warm_all_ready_pods(self, model_name: str):
        """遍历所有就绪的推理 Pod,执行预热"""
        pods: list[V1Pod] = self.k8s_api.list_namespaced_pod(
            namespace=self.namespace,
            label_selector="app=llm-inference"
        ).items

        tasks = []
        for pod in pods:
            if pod.status.phase == "Running" and pod.status.pod_ip:
                tasks.append(self.warm_up_pod(pod.status.pod_ip, model_name))

        results = await asyncio.gather(*tasks, return_exceptions=True)
        success_count = sum(1 for r in results if r is True)
        print(f"Warmup completed: {success_count}/{len(tasks)} pods ready")

四、GPU 调度的架构权衡与边界

Time-Slicing vs MPS vs DRA,怎么选?

  • Time-Slicing:最简单,但延迟抖动大。时间片切换有开销,不适合延迟敏感的在线推理。适合批量离线推理
  • MPS:共享上下文减少切换开销,但一个进程崩溃可能影响同 GPU 上其他进程。适合同构模型多实例场景
  • DRA:最灵活,K8s 原生支持,但 1.26+ 才稳定,生态工具链不成熟。适合新项目

弹性伸缩的天花板

  • GPU 节点扩容慢:从 0 启动一个 GPU 节点要 3-5 分钟(驱动初始化 + GPU 自检),HPA 等不起
  • 模型加载是瓶颈:72B 模型从磁盘加载到 GPU 要 30-60 秒,这段时间 Pod 无法服务
  • 预热策略有成本:保持备用 Pod 意味着 GPU 空转,不预热意味着突发流量下响应慢

禁用场景

  • 超大模型(>80GB 单卡显存)不适合 Time-Slicing,时间片切换会导致显存换入换出
  • 多租户环境慎用 MPS,进程间隔离性差,一个租户的异常请求可能拖垮其他租户
  • DRA 目前不支持 GPU 拓扑感知的自动优化,NVLink 拓扑需要手动配置

五、总结

AI 推理上云的核心挑战是 GPU 资源的刚性与推理负载的弹性之间的矛盾。Time-Slicing 适合低延迟不敏感的批量场景,MPS 适合同构模型多实例,DRA 是未来方向但生态尚不成熟。生产部署必须解决三个问题:GPU 资源池化与共享调度、模型预热与冷启动优化、基于自定义指标的弹性伸缩。vLLM 配合 K8s HPA 和 Prometheus 自定义指标可以实现基本的弹性推理服务,但 GPU 节点扩容延迟和模型加载耗时仍是架构瓶颈,需要通过预热策略和资源预留来缓解。

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐