AI 云原生架构实战:大模型推理服务的 K8s 弹性调度与 GPU 资源治理

AI2小时前发布 beixibaobao
3 0 0

AI 云原生架构实战:大模型推理服务的 K8s 弹性调度与 GPU 资源治理

一、GPU 利用率低迷与推理延迟波动的生产困局

大模型推理服务上线后,最直观的感受就是:GPU 很贵,但利用率很低。线上 A100 集群的平均 GPU 利用率长期在 35% 左右徘徊,而 P99 推理延迟却从 200ms 波动到 1.2s。流量低谷时 GPU 大量闲置,流量高峰时请求排队严重。根本原因在于传统 K8s 调度器对 GPU 资源的理解停留在"整卡分配"层面,缺乏对显存碎片、多实例共享和弹性伸缩的精细治理能力。本文直接给出从 GPU 资源池化到弹性调度的完整方案。

二、GPU 资源模型与 K8s 调度扩展的底层机制

K8s 原生调度器只识别 CPU 和 Memory 两种资源。GPU 通过 device-plugin 机制扩展注册,NVIDIA 的 device-plugin 默认以整卡为单位上报资源(nvidia.com/gpu: 1),调度器按整数卡做分配决策。

flowchart TD
    A[Pod 请求 nvidia.com/gpu] --> B[K8s 调度器 Filter 阶段]
    B --> B1{节点剩余 GPU 数 >= 请求数?}
    B1 -->|否| C[节点被过滤]
    B1 -->|是| D[Score 阶段: 选择 GPU 数最充裕的节点]
    D --> E[Bind: 绑定 Pod 到节点]
    E --> F[kubelet 调用 device-plugin Allocate]
    F --> G[NVIDIA device-plugin 返回 GPU 设备路径]
    G --> H[容器通过 NVIDIA_VISIBLE_DEVICES 环境变量访问 GPU]
    H --> I[容器内 nvidia-smi 可见对应 GPU]
    subgraph "问题:整卡分配的浪费"
        J[Pod A: 显存占用 10GB / 总量 80GB]
        K[剩余 70GB 显存无法被其他 Pod 使用]
    end

核心矛盾:一个推理服务可能只需要 10GB 显存,但 A100 有 80GB。整卡分配意味着 70GB 显存被白白浪费。NVIDIA MPS(Multi-Process Service)和 MIG(Multi-Instance GPU)是两种解决路径,但适用场景不同。

MIG 是硬件级分区,将一张 A100 切成最多 7 个实例,每个实例有独立的 SM 和显存,互不干扰。MPS 是软件级共享,多个进程共享同一 GPU 的 SM,通过时间片调度实现并发,但缺乏硬隔离。

三、GPU 资源池化与弹性调度的生产级实现

3.1 基于 MIG 的 GPU 分区配置

在 A100/H100 上启用 MIG,需要在节点级别配置:

# nvidia-device-plugin ConfigMap,启用 MIG 分区策略
apiVersion: v1
kind: ConfigMap
metadata:
  name: nvidia-device-plugin-config
  namespace: kube-system
data:
  config.yaml: |
    version: v1
    flags:
      migStrategy: mixed        # mixed 模式:同时暴露整卡和 MIG 实例
    sharing:
      timeSlicing:
        resources:
          - name: nvidia.com/gpu
            replicas: 4         # 每张 GPU 的时间片分片数

启用 mixed 策略后,device-plugin 会同时注册两种资源:

  • nvidia.com/gpu:整卡资源,用于大模型全量推理
  • nvidia.com/mig-1g.10gb:MIG 实例,用于轻量级推理或嵌入模型

Pod 按需声明:

# 使用 MIG 实例的轻量推理服务
apiVersion: apps/v1
kind: Deployment
metadata:
  name: embedding-service
  namespace: ai-inference
spec:
  replicas: 6
  selector:
    matchLabels:
      app: embedding-service
  template:
    metadata:
      labels:
        app: embedding-service
    spec:
      containers:
      - name: inference
        image: ai-inference/embedding:v2.3.1
        resources:
          limits:
            nvidia.com/mig-1g.10gb: 1   # 申请 1 个 MIG 实例(1g.10gb)
          requests:
            nvidia.com/mig-1g.10gb: 1
        env:
        - name: MODEL_NAME
          value: "bge-large-zh-v1.5"
        - name: MAX_BATCH_SIZE
          value: "32"

3.2 基于 Triton Inference Server 的动态批处理

单请求串行推理是 GPU 利用率低的另一个原因。Triton 的动态批处理(Dynamic Batching)将短时间窗口内的多个请求合并为一个 batch,大幅提升 GPU 吞吐:

# model_config.py — Triton 模型配置
model_config = {
    "name": "llm-inference",
    "platform": "tensorrt_llm",
    "max_batch_size": 64,
    "dynamic_batching": {
        "preferred_batch_size": [8, 16, 32, 64],
        "max_queue_delay_microseconds": 5000,  # 最多等 5ms 凑 batch
        "preserve_ordering": True               # 保证请求顺序
    },
    "instance_group": [
        {
            "count": 2,                         # 2 个模型实例
            "kind": "KIND_GPU",
            "gpus": [0]                         # 绑定到 GPU 0
        }
    ],
    "optimization": {
        "cuda": {
            "graphs": True                      # 启用 CUDA Graph 减少内核启动开销
        }
    }
}

max_queue_delay_microseconds 是关键参数:设太小,batch 凑不满;设太大,延迟上升。生产环境建议从 2000us 起步,根据 P99 延迟和吞吐指标逐步调整。

3.3 基于 KEDA 的 GPU 弹性伸缩

HPA 基于 CPU 指标做伸缩,但 GPU 推理服务的瓶颈在 GPU 利用率和请求队列长度。KEDA 支持自定义指标触发伸缩:

# KEDA ScaledObject — 基于 Triton 队列深度弹性伸缩
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: llm-inference-scaler
  namespace: ai-inference
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicaCount: 2
  maxReplicaCount: 12
  cooldownPeriod: 60            # 缩容冷却期 60s,避免频繁抖动
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus.monitoring:9090
      metricName: triton_inference_queue_size
      threshold: "8"            # 队列深度超过 8 触发扩容
      query: |
        avg(triton_inference_queue_size{model="llm-inference"})
  - type: prometheus
    metadata:
      serverAddress: http://prometheus.monitoring:9090
      metricName: gpu_utilization
      threshold: "75"           # GPU 利用率超过 75% 触发扩容
      query: |
        avg(DCGM_FI_DEV_GPU_UTIL{gpu="0"})

3.4 GPU 显存监控与 OOM 防护

GPU OOM 不像 CPU OOM 那样有明确的 throttle 机制,直接导致进程崩溃。需要主动监控显存使用并设置安全水位:

# gpu_memory_watcher.py — 显存水位监控与主动降级
import subprocess
import logging
import requests
ALERT_THRESHOLD = 0.85  # 显存使用率 85% 触发告警
DOWNGRADE_THRESHOLD = 0.95  # 95% 触发主动降级(减小 batch_size)
def get_gpu_memory_usage():
    """通过 nvidia-smi 获取显存使用率"""
    result = subprocess.run(
        ["nvidia-smi", "--query-gpu=memory.used,memory.total",
         "--format=csv,noheader,nounits"],
        capture_output=True, text=True
    )
    used, total = map(int, result.stdout.strip().split(", "))
    return used / total
def reduce_batch_size():
    """向 Triton 发送配置更新,减小 max_batch_size"""
    # 通过 Triton HTTP API 动态调整批处理参数
    try:
        resp = requests.post(
            "http://triton:8000/v2/modelconfig/llm-inference",
            json={"max_batch_size": 16}  # 从 64 降到 16
        )
        logging.warning("GPU 显存接近上限,已主动降低 batch_size 至 16")
    except Exception as e:
        logging.error(f"降级失败: {e}")
while True:
    usage = get_gpu_memory_usage()
    if usage > DOWNGRADE_THRESHOLD:
        reduce_batch_size()
    elif usage > ALERT_THRESHOLD:
        logging.warning(f"GPU 显存使用率: {usage:.1%},接近告警水位")
    time.sleep(5)

四、GPU 池化的代价与架构权衡

MIG 分区方案的最大限制是硬件绑定:只有 A100 80GB 和 H100 支持 MIG,V100、T4 等卡无法使用。MIG 实例一旦划分,修改需要重启节点上的所有 GPU 工作负载。MIG 实例的规格也是固定的(1g.10gb、2g.20gb 等),无法按需自定义显存大小。

动态批处理的权衡在于延迟与吞吐的博弈。凑 batch 必然引入等待延迟,对实时性要求极高的场景(如对话式 AI 的首 token 延迟)不适用。preserve_ordering 开启后,Triton 需要维护请求顺序,增加了调度开销。

KEDA 弹性伸缩的冷启动问题不可忽视。GPU 推理服务的 Pod 启动需要加载模型权重到显存,大模型(70B 参数)的加载时间可能超过 60 秒。这意味着流量突增时,新 Pod 无法立即承接请求。解决方案是预留 warm pool 或使用模型预热机制。

适用边界:MIG + Triton + KEDA 的组合方案适用于吞吐优先的批处理推理场景(如嵌入向量生成、批量文本分类)。对于延迟优先的在线推理场景(如对话生成),建议使用整卡分配 + 预留副本 + 较小的 max_batch_size。

五、总结

AI 云原生架构的核心挑战是 GPU 资源的精细化治理。从整卡分配到 MIG 分区,从串行推理到动态批处理,从 HPA 到 KEDA 自定义指标伸缩,每一步都在解决"GPU 昂贵但利用率低"这个核心矛盾。生产落地路径:先通过 Triton 动态批处理提升单卡吞吐,再用 MIG 分区实现多服务共享 GPU,最后接入 KEDA 实现基于队列深度的弹性伸缩。全程需要配套 GPU 显存水位监控和主动降级机制,防止 OOM 导致服务中断。

© 版权声明

相关文章