Kubernetes 上跑 AI 工作负载——GPU 调度和资源隔离
Kubernetes 上跑 AI 工作负载——GPU 调度和资源隔离
我第一次在 K8s 上部署本地模型推理服务的时候,犯了一个很蠢的错误:我把 GPU 资源当成 CPU 来管理,直接给 Pod 配了 resources.limits.cpu: "4" 和 resources.limits.memory: "16Gi",然后发现 Pod 起来了,模型也加载了,但推理速度奇慢无比——因为模型在用 CPU 跑,根本没用到 GPU。
原因是我忘了在 Pod Spec 里声明 GPU 资源请求。K8s 不会自动把 GPU 分配给 Pod,必须显式声明 nvidia.com/gpu: 1。
这个教训让我认识到:在 K8s 上运行 AI 工作负载,和跑普通 Java 服务有很多本质区别。这篇文章把核心知识点梳理清楚。
K8s GPU 调度的基本原理
K8s 本身不直接管理 GPU,它通过 Device Plugin 机制来扩展对硬件资源的感知和调度。
NVIDIA 提供了官方的 nvidia-device-plugin,以 DaemonSet 方式运行在每个有 GPU 的节点上,负责:
- 向 K8s API Server 注册该节点的 GPU 数量(作为扩展资源
nvidia.com/gpu) - 当 Pod 调度到该节点时,负责将具体的 GPU 设备挂载到容器里
安装 NVIDIA Device Plugin
在已有 GPU 节点的集群上,通过 Helm 安装:
# 添加 NVIDIA Helm 仓库
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
# 安装 Device Plugin
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--version 0.14.1
# 验证安装:检查节点是否有 GPU 资源
kubectl describe nodes | grep "nvidia.com/gpu"
# 期望输出类似:
# nvidia.com/gpu: 4 (可分配的 GPU 数量)GPU 工作负载的 K8s Manifest 示例
下面是一个完整的 AI 推理服务部署配置,我用 vLLM 部署 Llama-3 8B 为例:
# GPU 推理服务 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: llama3-inference
namespace: ai-inference
labels:
app: llama3-inference
tier: ai-serving
spec:
replicas: 1 # GPU 推理服务通常不水平扩展,而是垂直扩展(多 GPU)
selector:
matchLabels:
app: llama3-inference
template:
metadata:
labels:
app: llama3-inference
spec:
# 调度约束:只调度到有 GPU 的节点
nodeSelector:
accelerator: nvidia-a100 # 自定义标签,精确控制部署到哪类 GPU 节点
# 容忍 GPU 节点的污点(GPU 节点通常有 taint,防止普通服务占用 GPU 资源)
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
# 反亲和性:多副本时避免所有推理服务调度到同一节点
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: llama3-inference
topologyKey: kubernetes.io/hostname
# 模型文件存储(从 PVC 挂载,避免每次拉取模型文件)
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: llama3-model-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: 10Gi # vLLM 需要共享内存
containers:
- name: vllm-server
image: vllm/vllm-openai:v0.4.2
command:
- python3
- -m
- vllm.entrypoints.openai.api_server
args:
- "--model"
- "/models/Meta-Llama-3-8B-Instruct"
- "--tensor-parallel-size"
- "1" # 单 GPU
- "--max-model-len"
- "8192"
- "--port"
- "8000"
- "--host"
- "0.0.0.0"
ports:
- containerPort: 8000
name: http
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1" # 必须声明!K8s 才会分配 GPU
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1" # GPU 限制通常和请求一致(GPU 是独占资源)
volumeMounts:
- name: model-storage
mountPath: /models
- name: shm
mountPath: /dev/shm # 共享内存挂载点
# 健康检查(等模型加载完成才开始接流量,模型加载可能需要 1-3 分钟)
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120 # 等待模型加载
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 180
periodSeconds: 30
failureThreshold: 3
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
---
# Service 暴露推理接口
apiVersion: v1
kind: Service
metadata:
name: llama3-inference-svc
namespace: ai-inference
spec:
selector:
app: llama3-inference
ports:
- port: 8000
targetPort: 8000
name: http
type: ClusterIP
---
# 模型文件 PVC(提前拉取模型到 PV,避免 Pod 启动时拉模型)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: llama3-model-pvc
namespace: ai-inference
spec:
accessModes:
- ReadOnlyMany # 多副本只读共享
resources:
requests:
storage: 50Gi
storageClassName: local-ssd # 使用本地 SSD,降低模型加载延迟Namespace 配额:隔离不同团队的 GPU 资源
多个团队共享 GPU 集群时,必须通过 ResourceQuota 进行隔离,防止某个团队把所有 GPU 全部占用:
# 为 AI 推理团队分配 GPU 配额
apiVersion: v1
kind: ResourceQuota
metadata:
name: ai-inference-quota
namespace: ai-inference
spec:
hard:
# GPU 资源配额
requests.nvidia.com/gpu: "8" # 最多申请 8 块 GPU
limits.nvidia.com/gpu: "8"
# CPU 和内存配额
requests.cpu: "32"
requests.memory: "128Gi"
limits.cpu: "64"
limits.memory: "256Gi"
# 资源对象数量限制
pods: "20"
services: "10"
---
# 为 AI 训练团队分配单独的配额(训练任务需要更多 GPU)
apiVersion: v1
kind: ResourceQuota
metadata:
name: ai-training-quota
namespace: ai-training
spec:
hard:
requests.nvidia.com/gpu: "16"
limits.nvidia.com/gpu: "16"
requests.cpu: "64"
requests.memory: "256Gi"
limits.cpu: "128"
limits.memory: "512Gi"
pods: "10"
---
# LimitRange:强制每个 Pod 必须声明资源 limits(防止 Pod 无限制消耗资源)
apiVersion: v1
kind: LimitRange
metadata:
name: ai-inference-limits
namespace: ai-inference
spec:
limits:
- type: Container
default:
cpu: "2"
memory: "8Gi"
defaultRequest:
cpu: "1"
memory: "4Gi"
max:
cpu: "16"
memory: "64Gi"
nvidia.com/gpu: "4"GPU 节点的污点和容忍:防止普通服务占用 GPU
GPU 节点很贵,不能让普通的 Web 服务 Pod 被随机调度到 GPU 节点上占用资源:
# 给 GPU 节点打污点(taint)
kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule
kubectl taint nodes gpu-node-2 nvidia.com/gpu=present:NoSchedule
# 验证污点
kubectl describe node gpu-node-1 | grep -A5 "Taints"有了这个 taint,普通 Pod 默认无法调度到 GPU 节点。只有在 Pod Spec 里声明了对应 toleration 的 Pod(如上面的 Manifest 所示)才能被调度过去。
多 GPU 场景:Tensor Parallel 和 Pipeline Parallel
对于更大的模型(比如 Llama-3 70B),单块 A100 的 80GB 显存不够,需要多 GPU:
# 多 GPU 推理:使用 4 块 GPU 跑 70B 模型
containers:
- name: vllm-server-70b
image: vllm/vllm-openai:v0.4.2
args:
- "--model"
- "/models/Meta-Llama-3-70B-Instruct"
- "--tensor-parallel-size"
- "4" # 使用 Tensor Parallelism,4 块 GPU 协同
- "--max-model-len"
- "4096"
resources:
requests:
nvidia.com/gpu: "4" # 申请 4 块 GPU
limits:
nvidia.com/gpu: "4"
env:
- name: NCCL_DEBUG
value: "WARN" # GPU 间通信调试(nccl 是 NVIDIA 的 GPU 通信库)多 GPU Pod 调度时,K8s 会确保这 4 块 GPU 都在同一节点上(因为 GPU 间通信需要走 NVLink,跨节点太慢)。
HPA:AI 推理服务能做水平扩展吗?
这个问题很多人问过我。答案是:可以,但和普通服务的 HPA 不一样。
普通服务 HPA:根据 CPU 使用率增减 Pod 数量。
AI 推理服务 HPA 的挑战:
- GPU 是独占资源,扩缩容不像 CPU 那样粒度细
- 每个 Pod 启动都需要 1-3 分钟加载模型,响应时间太长
- 扩容意味着需要有空闲 GPU 节点,云上才方便(自建机房可能根本没有备用 GPU)
实际可行的方案是基于请求队列长度做 HPA,而不是基于 CPU 使用率:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: llama3-hpa
namespace: ai-inference
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: llama3-inference
minReplicas: 1
maxReplicas: 4
metrics:
# 基于自定义指标:推理请求队列深度
- type: External
external:
metric:
name: inference_queue_depth
selector:
matchLabels:
app: llama3-inference
target:
type: AverageValue
averageValue: "10" # 队列深度超过 10 时扩容
behavior:
scaleUp:
# 扩容慢一点:等待新 Pod 真正 Ready(模型加载)
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120 # 每 2 分钟最多扩 1 个 Pod
scaleDown:
# 缩容更保守:避免频繁缩扩
stabilizationWindowSeconds: 600模型预加载:解决 Pod 冷启动问题
AI 推理服务 Pod 冷启动慢的核心原因是模型文件需要从磁盘加载到 GPU 显存。减少冷启动时间的方法:
方法一:使用本地 PV(Local PersistentVolume)
把模型文件存在每个 GPU 节点的本地 SSD 上,避免网络传输:
apiVersion: v1
kind: PersistentVolume
metadata:
name: llama3-model-local-pv-node1
spec:
capacity:
storage: 100Gi
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
storageClassName: local-ssd
local:
path: /nvme/models/llama3 # GPU 节点的本地 NVMe 路径
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- gpu-node-1 # 绑定到具体节点方法二:使用 Init Container 预下载模型
如果模型存在 S3/OSS 上,用 Init Container 提前拉取:
initContainers:
- name: model-downloader
image: amazon/aws-cli:latest
command:
- aws
- s3
- sync
- s3://my-model-bucket/llama3-8b/
- /models/llama3-8b/
volumeMounts:
- name: model-storage
mountPath: /models
resources:
requests:
cpu: "2"
memory: "4Gi"监控 GPU 工作负载
GPU 推理服务需要额外监控 GPU 相关指标,用 DCGM Exporter(NVIDIA 官方 Prometheus Exporter):
# DCGM Exporter DaemonSet(采集 GPU 指标)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: dcgm-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: dcgm-exporter
template:
metadata:
labels:
app: dcgm-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9400"
spec:
nodeSelector:
accelerator: nvidia-a100 # 只在 GPU 节点上运行
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: dcgm-exporter
image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.5-3.4.0-ubuntu22.04
ports:
- containerPort: 9400
securityContext:
privileged: true # 需要访问 GPU 设备
resources:
requests:
cpu: "100m"
memory: "128Mi"关键的 GPU 监控指标:
DCGM_FI_DEV_GPU_UTIL → GPU 利用率(类比 CPU 使用率)
DCGM_FI_DEV_FB_USED → 显存使用量
DCGM_FI_DEV_FB_FREE → 显存剩余量
DCGM_FI_DEV_POWER_USAGE → 功耗(推理服务功耗高说明负载大)
DCGM_FI_DEV_GPU_TEMP → GPU 温度(过高会降频)
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE → Tensor Core 利用率(反映计算效率)一个实际踩过的坑:OOM 和 CUDA Out of Memory
在 K8s 上跑 GPU 推理,最常见的故障是两种 OOM:
1. 系统内存 OOM(Pod 被 K8s Kill) 表现:Pod 被 Kill,Events 里有 OOMKilled 记录。 原因:resources.limits.memory 设置太小,KV Cache 的系统内存部分超限。 解决:适当调大 memory limit,或者减小 --max-model-len 参数。
2. GPU 显存 OOM(CUDA out of memory) 表现:推理服务进程崩溃,日志里出现 RuntimeError: CUDA out of memory。 原因:模型本身 + KV Cache 占用超过了 GPU 显存。 解决:
- 减小
--gpu-memory-utilization(vLLM 参数,控制 KV Cache 使用比例,默认 0.9,可以改成 0.8) - 减小
--max-model-len(上下文长度) - 使用量化模型(AWQ/GPTQ,可以减少约 50% 显存占用)
# 查看 GPU 显存使用情况
kubectl exec -it llama3-inference-xxx -n ai-inference -- nvidia-smi
# 实时监控 GPU 使用
watch -n 1 kubectl exec -it llama3-inference-xxx -n ai-inference -- nvidia-smi小结
在 K8s 上运行 AI 工作负载,核心要点:
- Device Plugin 是关键:必须安装 nvidia-device-plugin,Pod 才能感知和使用 GPU
- 显式声明 GPU 资源:
nvidia.com/gpu: 1在 requests 和 limits 里都要写 - GPU 节点打 taint:防止普通服务抢占 GPU 资源
- ResourceQuota 按团队隔离:多团队共享集群时必须做配额管控
- 本地 PV 加速冷启动:模型文件放在节点本地 SSD,加载时间从几分钟缩短到几十秒
- DCGM 监控:GPU 利用率、显存使用、温度都要监控
把这些做好,K8s 上的 AI 推理服务就能达到和传统服务相似的运维水准。
