K8s 调度 GPU 推理
NVIDIA Device Plugin、GPU 节点池、HPA 弹性扩缩、模型热加载
K8s GPU 推理概述
在 AI 推理场景中,GPU 资源是成本最高的瓶颈之一。将 AI 推理服务运行在 Kubernetes(K8s)之上,能够充分利用容器编排的能力解决以下几个核心问题。
资源池化: 传统的 GPU 服务器通常以物理机或虚拟机形式部署,资源利用率低下。K8s 将 GPU 视为可调度的扩展资源,多个推理服务可以共享同一批 GPU 节点,提高整体利用率。
弹性伸缩: 推理服务的流量波动往往很大——白天高峰期请求量激增,夜间低谷期几乎无流量。K8s 的 Horizontal Pod Autoscaler(HPA)结合 Cluster Autoscaler 可以在流量上升时自动扩容推理 Pod 和 GPU 节点,在流量下降时回收资源,显著降低硬件成本。
滚动更新: 模型迭代更新频繁,K8s 的 Deployment 滚动更新机制允许在零停机的情况下完成模型版本切换,配合 readiness probe 确保流量只打到就绪的 Pod。
多租户隔离: 同一个 GPU 集群可能服务于多个业务线。通过 K8s 的命名空间(Namespace)、ResourceQuota 和 RBAC 机制,可以实现资源隔离和配额管理。
NVIDIA Device Plugin
工作原理
NVIDIA Device Plugin 是 NVIDIA 官方提供的 K8s 扩展组件,以 DaemonSet 形式在每个 GPU 节点上运行。它通过 gRPC 接口向 Kubelet 上报节点上的 GPU 资源列表,Kubelet 随后将这些资源信息同步到 API Server。当用户提交申请 GPU 的 Pod 时,调度器根据节点上剩余的 nvidia.com/gpu 资源数量进行调度决策。
Device Plugin 的工作流程如下:
- 节点上的 Device Plugin 进程启动,扫描本地 NVIDIA 驱动发现的 GPU 设备。
- 通过 ListAndWatch gRPC 流持续向 Kubelet 上报可用 GPU 及健康状态。
- Kubelet 将 GPU 资源信息注册到 API Server 的节点状态中。
- 调度器根据 Pod 的
resources.limits中声明的nvidia.com/gpu数量过滤和打分节点。 - Pod 调度到目标节点后,Kubelet 通知 Device Plugin 分配具体的 GPU 设备。
- Device Plugin 将 GPU 设备号通过环境变量(如
NVIDIA_VISIBLE_DEVICES)注入到容器中。
安装配置
推荐使用 Helm 安装 NVIDIA Device Plugin:
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install nvidia-device-plugin \
nvidia/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--set gfd.enabled=true安装完成后,节点上将出现 nvidia.com/gpu 资源类型。可以通过以下命令验证:
kubectl get nodes -o json | jq '.items[].status.capacity'如果看到 nvidia.com/gpu 字段,说明 Device Plugin 已正常工作。
共享 GPU
在生产环境中,一个 GPU 通常需要被多个推理任务共享以提升利用率。NVIDIA 提供了三种主流拆分方式:
| 拆分方式 | 隔离级别 | 显存隔离 | 性能损耗 | 适用场景 |
|---|---|---|---|---|
| Time-slicing | 进程级 | 无 | 低 | 对延迟不敏感、请求量稳定的批处理推理 |
| MPS(Multi-Process Service) | 进程级 | 软限制 | 中 | 吞吐优先、可接受一定延迟波动的推理服务 |
| MIG(Multi-Instance GPU) | 硬件级 | 硬件隔离 | 极低 | 严格隔离要求的生产环境、多租户场景 |
Time-slicing 通过时间片轮转让多个进程共享 GPU,配置简单但无显存隔离,一个进程的显存泄露会影响其他进程。
MPS 由 NVIDIA 驱动层提供,能控制每个进程的 GPU 计算资源占比,支持设置显存上限(软限制),适合吞吐优先的场景。
MIG 硬件级切分(仅支持 A100、A30、H100、H200 等型号),将一块 GPU 物理分割为多个独立实例,每个实例拥有独立的显存和计算单元,隔离性最佳。
GPU Pod YAML 配置示例
以下示例展示一个申请 1 块 GPU 的推理 Pod:
apiVersion: v1
kind: Pod
metadata:
name: gpu-inference-pod
spec:
containers:
- name: inference-server
image: registry.example.com/inference-server:v1
resources:
limits:
nvidia.com/gpu: 1
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "all"在共享 GPU 场景下,可以结合 Time-slicing 配置让多个 Pod 共享同一块 GPU:
apiVersion: v1
kind: Pod
metadata:
name: shared-gpu-pod
spec:
containers:
- name: inference-worker
image: registry.example.com/inference-worker:v1
resources:
limits:
nvidia.com/gpu: 0.5 # 申请半个 GPU 的时间片GPU 节点池
节点池配置
大型 K8s 集群通常包含 CPU 节点和 GPU 节点组成的混合节点池。GPU 节点应通过节点选择器或亲和性规则与推理服务绑定。
使用 nodeSelector:
spec:
nodeSelector:
gpu-type: a100使用节点亲和性(推荐):
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.product
operator: In
values:
- NVIDIA-A100-SXM4-80GB
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: gpu-pool
operator: In
values:
- productionrequiredDuringSchedulingIgnoredDuringExecution 是硬约束,确保 Pod 只调度到指定 GPU 型号的节点;preferredDuringSchedulingIgnoredDuringExecution 是软约束,优先选择标记为 production 的节点。
GPU 节点自动扩缩容
结合 Cluster Autoscaler,GPU 节点池可以实现自动扩缩容:
# Cluster Autoscaler 的节点池配置(以阿里云 ACK 为例)
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority
namespace: kube-system
data:
priorities: |-
{
"nodePools": {
"gpu-a100-pool": {
"minSize": 0,
"maxSize": 10,
"gpuType": "A100"
},
"gpu-v100-pool": {
"minSize": 0,
"maxSize": 5,
"gpuType": "V100"
}
}
}关键配置说明:
minSize: 0允许节点池完全缩容到零,在没有推理任务时不产生 GPU 节点费用。maxSize限制最大节点数,防止异常流量导致资源失控。- Cluster Autoscaler 会监控因资源不足而 Pending 的 Pod,当 GPU 资源不足时自动创建新节点。
Taint + Toleration
为防止非 GPU 负载调度到 GPU 节点,必须给 GPU 节点添加污点(Taint),并在推理 Pod 中添加对应的容忍度(Toleration)。
给 GPU 节点添加污点:
kubectl taint nodes gpu-node-1 nvidia.com/gpu=true:NoSchedule推理 Pod 配置容忍度:
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"这样配置后,非 GPU 负载因为没有对应的容忍度,不会被调度到 GPU 节点上,从而确保 GPU 资源专用于推理任务。
HPA 弹性扩缩
基于自定义指标的 HPA
推理服务的流量波动性大,仅靠 CPU/内存指标无法准确反映真实负载。推荐基于自定义指标(Custom Metrics)进行弹性扩缩。
以下示例展示基于请求排队长度的 HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: inference-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: inference-server
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: inference_queue_depth
target:
type: AverageValue
averageValue: 10
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15配置要点:
inference_queue_depth是推理框架(如 Triton Inference Server 或 TorchServe)暴露的自定义指标,表示当前等待处理的请求数量。scaleDown设置 300 秒的稳定窗口和 10% 的速率限制,防止抖动导致频繁缩容。scaleUp设置 0 秒窗口并允许 100% 的扩容速率,确保流量突增时快速响应。
Readiness Probe 与滚动更新
推理 Pod 启动时可能需要进行模型加载,这个过程可能耗时数秒到数分钟。必须配置合适的 readiness probe 确保流量不会打到未就绪的 Pod:
spec:
containers:
- name: inference-server
readinessProbe:
httpGet:
path: /v2/health/ready
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /v2/health/live
port: 8000
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 6initialDelaySeconds给模型加载预留充足时间。failureThreshold设置为较大值,避免因模型加载短暂超时导致 Pod 被重启。
模型热加载
模型下载 Sidecar 模式
推理镜像不应将模型文件打包在内,否则每次模型更新都需要重新构建和推送镜像。推荐使用 sidecar 模式或 init container 实现模型动态加载。
使用 init container 下载模型:
spec:
initContainers:
- name: model-downloader
image: alpine:3.19
command:
- sh
- -c
- |
wget -qO- https://models.example.com/v3/resnet50.tar.gz | \
tar xz -C /models/resnet50
volumeMounts:
- name: model-data
mountPath: /models
containers:
- name: inference-server
image: registry.example.com/inference-server:v1
volumeMounts:
- name: model-data
mountPath: /models
readOnly: true
volumes:
- name: model-data
emptyDir: {}init container 负责从远端存储下载模型文件到 emptyDir 共享卷,推理容器以只读方式挂载。当模型版本变更时,只需要更新 init container 的下载脚本或镜像标签。
模型版本管理
对于需要持久化存储模型的场景,使用 PV/PVC 挂载是更好的选择:
apiVersion: v1
kind: PersistentVolume
metadata:
name: models-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
nfs:
server: nfs-server.example.com
path: /models
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: models-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi推荐的文件结构:
/models/
resnet50/
v1/
config.json
model.pt
v2/
config.json
model.pt
bert-base/
v1/
config.json
model.bin每个模型按版本号组织目录,推理服务通过环境变量或启动参数指定使用哪个版本。
滚动更新策略
Deployment 的滚动更新策略需要根据 GPU 资源特点进行调整:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-server
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
template:
# ... Pod 模板maxSurge: 1表示更新时最多比期望副本数多启动 1 个 Pod。GPU 资源昂贵,限制 surge 数量可以避免资源浪费。maxUnavailable: 1表示更新过程中最多允许 1 个 Pod 不可用,确保服务质量。
如果模型文件通过 PV/PVC 挂载,滚动更新时新 Pod 可以直接挂载新版模型,无需重新下载。此时建议将 maxSurge 设置为 0 并使用 maxUnavailable: 1 实现蓝绿部署式的平滑切换。