AI 云原生架构实战:大模型推理服务的 K8s 弹性调度与 GPU 资源治理
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 导致服务中断。