从单卡训练 Job 到 vLLM 推理服务,讲清 Pod、Deployment、PVC、GPU 资源声明与排障顺序,并提供可拆分使用的完整 YAML。
给 AI 工程师的 Kubernetes 速通:概念与最小可用方案
在单机上,启动训练通常只需要一条命令;进入共享 GPU 集群后,还要回答几个问题:任务放在哪个节点,申请多少资源,失败后是否重试,检查点存在哪里,推理服务什么时候可以接收请求?
Kubernetes(下文简称 K8s)用一组声明式对象管理这些事情。对 AI 工程师来说,入门可以从两条路径开始:训练和批处理用 Job,持续提供请求响应的推理服务用 Deployment + Service。
本文不涉及集群安装。先理解对象关系,再跑一个会保存检查点的单卡训练示例,最后部署一个小模型推理服务。文末给出扩展到多卡、多副本时必须重新检查的条件。
说明
适用范围:已有 NVIDIA GPU 集群,采用 device plugin 暴露的 nvidia.com/gpu 扩展资源;示例使用 Linux amd64 节点。启用了 MIG、GPU 共享或 DRA 的集群,应按平台实际资源模型调整。本文依据官方文档整理并做静态校验,不将示例视为已在读者集群完成的部署验证。
一、只记住与工作流直接相关的对象
| 对象 | 解决什么问题 | AI 项目中的典型用途 |
|---|---|---|
| Pod | 将一个或多个紧密协作的容器一起调度到一个节点;共享网络,可挂载共享卷 | 一次训练进程、一份推理实例 |
| Job | 追踪有限任务的完成状态,并按策略处理失败 | 微调、评测、批量推理 |
| CronJob | 按时间表创建 Job | 定时评测、周期性数据处理 |
| Deployment | 持续维持目标副本数,管理 Pod 更新 | 可替换实例的在线推理服务 |
| StatefulSet | 为 Pod 提供稳定身份;可配合卷声明模板提供持久存储 | 确实依赖稳定身份或独立持久卷的服务 |
| Service | 用稳定的服务地址发现和访问一组 Pod | 将请求发送给可接流量的推理实例 |
| Ingress / Gateway API | 描述外部 HTTP 等流量的入口和路由 | 域名、TLS、路由到服务;需要相应控制器 |
| PVC / PV / StorageClass | 分别描述存储申请、存储资源和供应方式 | 数据集、模型缓存、训练检查点 |
| ConfigMap / Secret | 向容器提供普通配置或敏感数据 | 训练参数、访问模型仓库的凭据 |
| Namespace | 给对象分组,并作为权限、配额等策略的作用范围 | 区分团队和环境 |
StatefulSet 不是分布式训练的默认答案。稳定名字只解决其中一部分问题,多机训练仍要处理进程发现、rank、集合通信和失败恢复,通常结合训练控制器及调度方案完成。StatefulSet 官方说明
Namespace 本身不会自动建立网络隔离或资源配额,需要配合 RBAC、ResourceQuota 和受网络插件支持的 NetworkPolicy 等策略。Secret 的 base64 表达也不是加密,存储加密与访问控制要由平台配置。多租户说明 · Secret 官方说明
两条工作流,理解最常用的对象:一次性任务追踪完成状态;在线服务持续维持可接收请求的实例。
二、先确认集群具备运行条件
本文两个示例可以顺序运行,至少需要一份可分配的完整 GPU 资源,以及足够的 CPU、主机内存和持久存储。推理示例选择 Qwen2.5-0.5B-Instruct 这样的小模型,降低初次验证的资源门槛;显存需求仍受引擎版本、上下文长度和并发设置影响。
开始前确认以下条件:
- 节点的 NVIDIA 驱动、容器运行时和 device plugin 已由平台配置完成,GPU 已出现在节点可分配资源中。
- 当前账号可以在示例命名空间中创建工作负载和 PVC;节点、StorageClass 等集群级信息可能需要管理员协助查看。
- 集群有支持示例 RWO PVC 的默认 StorageClass;没有默认类时,在两个 PVC 中分别填写平台提供的
storageClassName。 - 节点能够拉取示例镜像,推理容器能够访问模型仓库及实际文件下载地址。受限网络中,应先准备内部镜像和模型缓存。
# 先确认当前操作的是哪个集群
kubectl config current-context
# 需要相应的集群级只读权限
kubectl get nodes -L kubernetes.io/arch,nvidia.com/gpu.product
kubectl get nodes -o custom-columns='NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu'
kubectl get storageclassallocatable 表示节点可供调度的总量,不等于当前空闲 GPU 数量;还要结合节点上已有 Pod 的资源申请查看。型号标签以实际节点为准,不要直接抄一个可能不存在的 H800A-SXM5 标签。GPU 节点如果设置了污点,或平台要求 runtimeClassName,应使用平台给出的具体配置。GPU 调度前提 · NVIDIA device plugin
创建命名空间,将下面内容保存为 manifests/00-namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: ai-demokubectl apply -f manifests/00-namespace.yaml三、kubectl:按照问题找命令
下面用 POD_NAME 表示真实 Pod 名称,使用前先替换;多容器 Pod 还需指定 -c。
| 目的 | 命令 |
|---|---|
| 看状态和所在节点 | kubectl -n ai-demo get pods -o wide |
| 看调度、挂载和容器终止信息 | kubectl -n ai-demo describe pod POD_NAME |
| 看当前日志 | kubectl -n ai-demo logs -f POD_NAME -c trainer |
| 看同一 Pod 中上一次容器实例的日志 | kubectl -n ai-demo logs POD_NAME -c trainer --previous |
| 进入仍在运行且带 shell 的容器 | kubectl -n ai-demo exec -it POD_NAME -c trainer -- sh |
| 提交声明 | kubectl apply -f manifests/01-train.yaml |
| 查看 CPU / 主机内存使用情况 | kubectl -n ai-demo top pod |
| 本地连接推理服务 | kubectl -n ai-demo port-forward service/vllm-demo 8000:80 |
--previous 适用于容器在同一个 Pod 内发生过重启的情况,不能读取已删除 Pod 的历史日志。生产任务需要集中采集日志。kubectl top 依赖 Metrics Server,默认也不会展示 GPU 利用率或显存;GPU 指标通常另接 DCGM 等采集组件。kubectl top
排障通常从 get 和 describe 开始,再按状态看日志或挂载事件。进程已经退出时,反复尝试 exec 不会提供更多信息。
四、一个会训练、会保存结果的单卡 Job
先用合成数据训练一个小型线性模型,验证 GPU 可见 → 计算成功 → 检查点落盘。它不下载外部数据,也不是 LoRA 教程;跑通后再替换成自己的训练镜像、配置文件和数据挂载。
将以下完整内容保存为 manifests/01-train.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: train-output
namespace: ai-demo
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: train-code
namespace: ai-demo
data:
train.py: |
import os
from pathlib import Path
import torch
assert torch.cuda.is_available(), "CUDA is not available"
torch.manual_seed(42)
device = torch.device("cuda")
print("GPU:", torch.cuda.get_device_name(0), flush=True)
x = torch.randn(1024, 32, device=device)
y = x.sum(dim=1, keepdim=True)
model = torch.nn.Linear(32, 1).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)
for step in range(100):
optimizer.zero_grad(set_to_none=True)
loss = torch.nn.functional.mse_loss(model(x), y)
loss.backward()
optimizer.step()
if step % 20 == 0:
print(f"step={step} loss={loss.item():.6f}", flush=True)
# 各 Pod 独立目录,避免重试或替代 Pod 覆盖同一路径
output = Path("/outputs") / os.environ["POD_UID"]
output.mkdir(parents=True, exist_ok=True)
temporary = output / "checkpoint.tmp"
torch.save(model.state_dict(), temporary)
temporary.replace(output / "checkpoint.pt")
print("saved:", output / "checkpoint.pt", flush=True)
---
apiVersion: batch/v1
kind: Job
metadata:
name: gpu-train-demo
namespace: ai-demo
spec:
backoffLimit: 0
activeDeadlineSeconds: 1800
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: trainer
image: pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime
command: [python, -u, /app/train.py]
env:
- name: POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
nvidia.com/gpu: "1"
volumeMounts:
- name: code
mountPath: /app
readOnly: true
- name: output
mountPath: /outputs
volumes:
- name: code
configMap:
name: train-code
- name: output
persistentVolumeClaim:
claimName: train-output示例镜像标签已核对存在,但版本固定不等于适合所有 GPU 或通过生产安全评估。投产时使用团队验证过的镜像,并固定 digest;严格的非 root 运行策略还需要镜像用户、卷权限与平台策略相匹配。PyTorch 镜像仓库
提交并查看结果:
kubectl apply --dry-run=server -f manifests/01-train.yaml
kubectl apply -f manifests/01-train.yaml
kubectl -n ai-demo get pods -l job-name=gpu-train-demo -w
# 看完状态后按 Ctrl+C,继续读取日志
kubectl -n ai-demo logs -f job/gpu-train-demo -c trainer
kubectl -n ai-demo wait --for=condition=complete job/gpu-train-demo --timeout=30m服务端 dry-run 检查 API 校验及支持该模式的准入规则,不会证明镜像能拉取、GPU 能分配或代码能运行。若等待超时,检查 Job 是否已经 Failed,再查看对应 Pod 的状态和日志。
四个字段,决定任务怎样运行
- **GPU 资源:**只写
limits.nvidia.com/gpu时,K8s 将其作为 request。也可以同时填写 requests 和 limits,但两者必须相等;不能只写 GPU request。 - **CPU / 内存:**requests 参与调度,limits 约束使用。主机内存与 GPU 显存是不同资源,
memory: 8Gi并不代表申请 8 GiB 显存。 - **
restartPolicy: Never:**不在原 Pod 内自动重启失败容器。backoffLimit: 0让此示例在计入失败后不再重试,便于保留错误现场。 - **
activeDeadlineSeconds:**给整个 Job 设置运行期限,排队和启动耗时也应纳入预算;真实训练要按任务时长调整。
Job 不提供“程序绝对只执行一次”的保证,结果写入仍需考虑重复执行和恢复。这个例子用 Pod UID 区分输出目录;真实训练应另外设计检查点恢复策略。修改 Job 的训练模板后,通常需要用新 Job 名称提交,不能指望对已完成 Job 执行 apply 就重新训练。Job 语义 · 容器资源管理
五、单卡推理:Deployment + Service + 探针
下面部署一份小模型实例,并用 PVC 缓存下载文件。首次启动需要访问模型仓库;缓存卷刚创建时是空的,不会自动带有模型。
为控制入门复杂度,示例使用 1 个副本、1 张 GPU、RWO 缓存卷和 Recreate 更新策略。更新时旧实例先停止,新实例再启动,因此会有服务中断;它是起步方案,不是高可用部署。
保存为 manifests/02-inference.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-cache
namespace: ai-demo
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-demo
namespace: ai-demo
spec:
replicas: 1
strategy:
type: Recreate
progressDeadlineSeconds: 2400
selector:
matchLabels:
app: vllm-demo
template:
metadata:
labels:
app: vllm-demo
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 120
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: vllm
image: vllm/vllm-openai:v0.11.0
args:
- --model=Qwen/Qwen2.5-0.5B-Instruct
- --revision=7ae557604adf67be50417f59c2c2f167def9a775
- --served-model-name=qwen-demo
- --host=0.0.0.0
- --port=8000
- --tensor-parallel-size=1
- --dtype=half
- --max-model-len=2048
- --gpu-memory-utilization=0.8
resources:
requests:
cpu: "2"
memory: 8Gi
limits:
cpu: "4"
memory: 16Gi
nvidia.com/gpu: "1"
ports:
- name: http
containerPort: 8000
startupProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 180
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
volumeMounts:
- name: cache
mountPath: /root/.cache/huggingface
- name: shm
mountPath: /dev/shm
volumes:
- name: cache
persistentVolumeClaim:
claimName: model-cache
- name: shm
emptyDir:
medium: Memory
sizeLimit: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: vllm-demo
namespace: ai-demo
spec:
type: ClusterIP
selector:
app: vllm-demo
ports:
- name: http
port: 80
targetPort: http镜像版本与模型 revision 用于固定教程输入,不代表最新或推荐生产版本。升级时要重新检查模型支持、GPU 架构和驱动兼容性。内存型 emptyDir 的实际使用会计入相关容器内存用量,sizeLimit 不是额外赠送的内存预算。vLLM Kubernetes 部署说明 · 内存资源与 emptyDir
启动探针与就绪探针各管一件事
startupProbe 给首次下载、加载和初始化留出约 30 分钟的失败容忍窗口;成功之前,readiness 不开始执行。窗口应根据实际冷启动时间调整,不是模型越大就无限延长。
readinessProbe 决定实例是否进入正常的 Service 流量路径,失败后会被标记为未就绪,不会因此重启容器。本示例没有配置 liveness:进程退出仍会按 Deployment Pod 的重启策略恢复;如果要处理进程存活但卡死的情况,应在负载测试后另设存活探针,避免高负载引起错误重启。探针行为
部署并等待就绪:
kubectl apply --dry-run=server -f manifests/02-inference.yaml
kubectl apply -f manifests/02-inference.yaml
kubectl -n ai-demo rollout status deployment/vllm-demo --timeout=40m
kubectl -n ai-demo get pods,pvc,service
kubectl -n ai-demo get endpointslices -l kubernetes.io/service-name=vllm-demo一个终端保持本地端口转发:
kubectl -n ai-demo port-forward service/vllm-demo 8000:80另开一个终端发送真实请求:
curl -sS http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"qwen-demo","messages":[{"role":"user","content":"用一句话解释 Kubernetes Pod。"}],"max_tokens":64}'/health 成功和真实推理返回结果是两个不同检查。端口转发适合本地验证,会转发到选定的 Pod,不能用来证明 Service 数据面负载均衡或外部入口已经正常。正式对外提供服务时,再接入平台已有的 Ingress / Gateway 控制器,并配置身份认证、TLS 与限流;单独创建 Ingress 对象不会自动安装控制器。Ingress 说明
六、扩展到多卡、多副本前,先算资源账
四卡 Pod 必须能放进同一个节点
在本文的资源模型下,一个 Pod 中的四卡容器,需要同一节点提供四份 GPU 资源。集群总共空闲四张卡,如果分散在四个节点,也不能满足这个 Pod。
若 replicas: 2,且每份实例申请 4 张 GPU,稳态就需要 8 张卡。更新时如果允许多创建 1 份实例,还需要暂时容纳额外 4 张卡,以及相应的 CPU、内存和卷挂载条件。
GPU 已用满时,可以选择预留更新容量,或允许先减少旧副本再启动新副本;后者会降低服务容量。Readiness 是控制流量接入的一环,不能单独保证零停机。生产还需验证连接排空、请求超时、故障域分布和容量余量。Deployment 更新策略
GPU 数量匹配,不代表通信拓扑最优
仅声明 nvidia.com/gpu: 4 不能表达期望的 NVLink 连接关系,也不保证分布式任务同时获得所需节点。Topology Manager 主要在 kubelet 侧协调 NUMA 资源亲和性;single-numa-node 在无法满足条件时可能拒绝 Pod,不能直接当作 NVLink 优化开关。
多卡和多机任务应结合节点实际拓扑、device plugin、网络与训练框架验证,再选择平台支持的拓扑或成组调度能力。不要为了“优化”直接修改整个集群的 NUMA 策略。Topology Manager 说明
RWO 不是“只能给一个 Pod”
ReadWriteOnce 表示卷可由单个节点读写挂载,同节点上的多个 Pod 仍可能使用它;只允许一个 Pod 的语义对应 ReadWriteOncePod,并有 CSI 支持条件。跨节点多副本通常需要支持相应访问模式的共享存储,或为各副本准备独立缓存。
因此,**不能只把本文推理的 replicas 从 1 改成 2,就认为已经完成扩容。**还要调整缓存布局和更新策略。RWX 也不会替应用解决并发写入竞争,多个训练 worker 不应无协调地覆盖同一个检查点文件。PV 访问模式
七、故障速查:先判断卡在哪一层
先判断卡在哪一步,再选择排障证据:从 get / describe 开始,结合日志、卷状态和服务端点定位问题。
| 现象 | 优先看什么 | 常见原因与动作 |
|---|---|---|
| Pending / FailedScheduling | Pod Events、节点资源申请、选择器与污点 | 单节点 GPU 不足、CPU / 内存不足、节点不匹配、缺少相应 toleration |
| PVC Pending / FailedMount / Multi-Attach | PVC、StorageClass、Pod Events | 无供应器、容量或拓扑不满足、卷仍挂在别处;WaitForFirstConsumer 场景下先 Pending 可能正常 |
| ImagePullBackOff | Events 中具体拉取错误 | 镜像名或标签错误、仓库认证失败、网络受限 |
| CrashLoopBackOff / Job Failed | 容器退出原因、当前日志与适用时的 --previous | 命令、配置、文件路径、权限、依赖或 CUDA 初始化失败 |
| OOMKilled / 退出码 137 | 容器 state / lastState、节点压力与应用日志 | OOMKilled 要结合状态确认;137 只说明进程收到 SIGKILL,不能单凭它判定内存溢出 |
| 日志出现 CUDA out of memory | 应用错误栈和 GPU 显存 | 与主机内存 OOM 区分;检查模型、KV cache、并发及共享 GPU 上的其他进程 |
| Running 但未 Ready | 容器日志、探针失败信息 | 仍在初始化、模型下载失败、端口错误或健康检查超时;Running 不等于可服务 |
| Service 无可用后端 | Pod 标签、Service selector、EndpointSlice 条件 | 标签不匹配、Pod 未就绪;再检查端口及网络策略 |
hostPort 会占用节点上的相应 IP / 端口组合,可能限制放置,但与“半张 GPU”没有直接关系。普通扩展 GPU 资源不能写 0.5;time-slicing、MIG 等共享方案需要平台配置,且各自的隔离语义不同。多数普通推理服务可以先通过 Service 暴露容器端口。NVIDIA GPU 共享说明
八、跑完后,保留结果并释放计算资源
训练 Job 完成后,容器不再运行,检查点保留在 train-output PVC 中。可以通过后续读取该 PVC 的任务归档到对象存储;Pod 日志不能替代训练产物。
确认不再需要运行推理,并已记录需要的日志后,停止示例工作负载:
kubectl -n ai-demo delete deployment vllm-demo
kubectl -n ai-demo delete service vllm-demo
kubectl -n ai-demo delete job gpu-train-demo这些命令保留本文单独创建的两个 PVC。删除 PVC 或整个 Namespace 可能进一步触发底层卷回收,是否保留数据取决于存储回收策略;归档前不要把删除整个目录下 YAML 当作通用“撤销”操作。
做到这里,你已经掌握了 AI 工作流最常用的一条完整路径:声明资源、运行任务、持久化结果、接入推理流量,再依据状态与日志定位故障。实际项目下一步应围绕已有工作负载补上检查点恢复、容量规划和发布验证,而不是一次性引入全部 K8s 功能。
最后更新于
