九章智算云
给 AI 工程师的 Kubernetes 速通:概念与最小可用方案

从单卡训练 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 官方说明

两条工作流,理解最常用的对象:一次性任务追踪完成状态;在线服务持续维持可接收请求的实例。

训练与批处理Job完成状态 / 失败策略创建与管理训练 Pod容器 + GPU / CPU / 主机内存训练成功后退出写入检查点PVC → PV持久存储Pod 结束,卷中的结果仍可保留。在线推理Deployment副本数 / 更新策略管理实例推理 Pod模型加载 → 探针通过 → Ready按需挂载模型或缓存 PVC接收请求Service稳定服务地址客户端未就绪实例不接正常 Service 流量。共同配套:Namespace 组织资源 / ConfigMap 注入配置 / Secret 提供凭据图 1:控制器管理 Pod,Service 组织访问,PVC 承接需保留的数据;入口、存储、权限与 GPU 能力依赖集群配置。

二、先确认集群具备运行条件

本文两个示例可以顺序运行,至少需要一份可分配的完整 GPU 资源,以及足够的 CPU、主机内存和持久存储。推理示例选择 Qwen2.5-0.5B-Instruct 这样的小模型,降低初次验证的资源门槛;显存需求仍受引擎版本、上下文长度和并发设置影响。

开始前确认以下条件:

  1. 节点的 NVIDIA 驱动、容器运行时和 device plugin 已由平台配置完成,GPU 已出现在节点可分配资源中。
  2. 当前账号可以在示例命名空间中创建工作负载和 PVC;节点、StorageClass 等集群级信息可能需要管理员协助查看。
  3. 集群有支持示例 RWO PVC 的默认 StorageClass;没有默认类时,在两个 PVC 中分别填写平台提供的 storageClassName。
  4. 节点能够拉取示例镜像,推理容器能够访问模型仓库及实际文件下载地址。受限网络中,应先准备内部镜像和模型缓存。
# 先确认当前操作的是哪个集群
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 storageclass

allocatable 表示节点可供调度的总量,不等于当前空闲 GPU 数量;还要结合节点上已有 Pod 的资源申请查看。型号标签以实际节点为准,不要直接抄一个可能不存在的 H800A-SXM5 标签。GPU 节点如果设置了污点,或平台要求 runtimeClassName,应使用平台给出的具体配置。GPU 调度前提 · NVIDIA device plugin

创建命名空间,将下面内容保存为 manifests/00-namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: ai-demo
kubectl 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 开始,结合日志、卷状态和服务端点定位问题。

01Pending / 调度失败看 Events、资源申请、选择器、污点与 PVC集群有空闲 GPU,不等于某个节点能装下整个 Pod。02等待启动 / 镜像拉取看镜像仓库错误、卷挂载和运行时事件先确认镜像、凭据、网络和挂载条件,再检查应用。03退出 / 反复重启看容器终止原因、当前日志与适用的历史日志退出码 137 不能单独证明 OOM;CUDA OOM 需看应用日志。04Running 但未 Ready看模型加载、探针、监听端口和 EndpointSlice进程运行中不等于可以接请求;readiness 失败不会重启。扩容前先算 GPU 预算2 副本 × 每份 4 卡 = 稳态 8 卡;若更新时多建 1 份实例,需临时容纳额外 4 卡。图 2:从 Pod 状态定位证据再修复,不要都归为 GPU 不够;预算按整卡分配示意,不含共享、MIG 等资源模型。
现象优先看什么常见原因与动作
Pending / FailedSchedulingPod Events、节点资源申请、选择器与污点单节点 GPU 不足、CPU / 内存不足、节点不匹配、缺少相应 toleration
PVC Pending / FailedMount / Multi-AttachPVC、StorageClass、Pod Events无供应器、容量或拓扑不满足、卷仍挂在别处;WaitForFirstConsumer 场景下先 Pending 可能正常
ImagePullBackOffEvents 中具体拉取错误镜像名或标签错误、仓库认证失败、网络受限
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 功能。

最后更新于

这篇文档对你有帮助吗?

目录