第30篇-Kubeflow-Training-Operator-K8s上的分布式训练

【AIaaS 全栈架构师】第 30 篇:Kubeflow Training Operator------K8s 上的分布式训练调度

系列定位:AIaaS 全栈架构师教程,技术栈以 Go 为主。本篇聚焦 K8s 如何用 Operator 模式管理分布式训练任务,以及 Gang Scheduling 如何解决 all-or-nothing 难题。


本篇你将学到

  • 为什么分布式训练任务不能用普通 Deployment/Job 管理
  • Training Operator 的控制器架构与 CRD 设计
  • PyTorchJob / TensorFlowJob CRD 的字段含义
  • 分布式训练任务的提交、状态流转、日志收集
  • Gang Scheduling 与 Volcano 的必要性
  • PyTorchJob 与 Ray Train、Kueue 的对比

上一篇我们解决了 GPU 共享。但分布式训练任务还有一个独特需求:all-or-nothing。一个 16 卡训练任务必须同时拿到 16 张 GPU 才能启动,否则就会出现「3 个 worker 已经起来,13 个还在 Pending,前面 3 个空耗 GPU」的死锁。原生的 K8s Job/Deployment 完全没有这种语义。Kubeflow Training Operator 就是为这个场景而生的。


一、为什么需要专门的训练任务控制器

1.1 分布式训练的「整批」语义

先看一个 PyTorch DDP(DistributedDataParallel)训练任务的标准启动姿势。它有 N 个 worker 进程,每个进程:

  1. 初始化进程组(torch.distributed.init_process_group),通过 rendezvous 互相发现。
  2. 等所有 rank 到齐,初始化 NCCL 通信。
  3. 进入训练循环,每个 step 做 AllReduce 同步梯度。

如果只有一个 worker 先启动,它会卡在 init_process_group,等其他 rank。这个等待不是无所谓的------它占着 GPU,可能等几十分钟甚至超时失败。问题严重性:

复制代码
任务: 16 个 worker, 每个占 1 张 A100
集群空闲 GPU: 10

方案A: 用 K8s Job(副本=16)
    - 调度器先后调度 10 个 worker 到有 GPU 的节点
    - 剩下 6 个 Pending
    - 已起 10 个 worker 卡在 init_process_group 等 6 个不会到的同伴
    - 等待 30 分钟超时 → CrashLoopBackOff → 释放 GPU
    - 期间 10 张 GPU 完全空转

方案B: 用 K8s Job(副本=16)+ 别的任务
    - 任务 X 抢到 3 张 GPU(剩余 7)
    - 任务 Y 抢到 3 张 GPU(剩余 4)
    - 三个任务谁都不够 16 张,全部卡死 → 集群利用率 0%

这就是 分布式训练的死锁问题 。解决之道是 Gang Scheduling(成组调度):要么所有 worker 一起调度,要么一个都不调度。

1.2 原生工作负载控制器的不足

K8s 原生对象 训练场景下的不足
Deployment 不支持 Pod 间通信拓扑,副本滚动会破坏训练一致性
Job 缺少 Gang 语义,副本可被部分调度
CronJob 周期触发,但单次执行仍是 Job
StatefulSet 有序但无 Gang,且不针对分布式训练任务语义

训练任务还有一堆特殊语义:worker/ps(parameter server)/chief/scheduler 不同角色、不同镜像、不同资源;要注入 WORLD_SIZERANKMASTER_ADDR 等环境变量;要管理 rendezvous endpoint;要做 checkpoint 续训。这些都需要一个 领域专用控制器

1.3 Operator 模式:声明式 + 控制循环

Kubeflow Training Operator 采用 K8s 的 Operator 模式

  1. 用户声明一个 CR(Custom Resource),描述训练任务期望的样子。
  2. Operator(一个 Controller 进程)watch 这类 CR。
  3. Operator 根据期望状态创建/更新 Pod、Service、ConfigMap。
  4. 不断 reconcile,直到实际状态收敛到期望状态。

声明式 + 控制循环 = 用户只描述「我要什么」,Operator 自动算「怎么做到」。


二、Training Operator 架构

2.1 整体架构

#mermaid-svg-JOejoFVOBGwy4JpJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JOejoFVOBGwy4JpJ .error-icon{fill:#552222;}#mermaid-svg-JOejoFVOBGwy4JpJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JOejoFVOBGwy4JpJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JOejoFVOBGwy4JpJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JOejoFVOBGwy4JpJ .marker.cross{stroke:#333333;}#mermaid-svg-JOejoFVOBGwy4JpJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JOejoFVOBGwy4JpJ p{margin:0;}#mermaid-svg-JOejoFVOBGwy4JpJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster-label text{fill:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster-label span{color:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster-label span p{background-color:transparent;}#mermaid-svg-JOejoFVOBGwy4JpJ .label text,#mermaid-svg-JOejoFVOBGwy4JpJ span{fill:#333;color:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ .node rect,#mermaid-svg-JOejoFVOBGwy4JpJ .node circle,#mermaid-svg-JOejoFVOBGwy4JpJ .node ellipse,#mermaid-svg-JOejoFVOBGwy4JpJ .node polygon,#mermaid-svg-JOejoFVOBGwy4JpJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JOejoFVOBGwy4JpJ .rough-node .label text,#mermaid-svg-JOejoFVOBGwy4JpJ .node .label text,#mermaid-svg-JOejoFVOBGwy4JpJ .image-shape .label,#mermaid-svg-JOejoFVOBGwy4JpJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-JOejoFVOBGwy4JpJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JOejoFVOBGwy4JpJ .rough-node .label,#mermaid-svg-JOejoFVOBGwy4JpJ .node .label,#mermaid-svg-JOejoFVOBGwy4JpJ .image-shape .label,#mermaid-svg-JOejoFVOBGwy4JpJ .icon-shape .label{text-align:center;}#mermaid-svg-JOejoFVOBGwy4JpJ .node.clickable{cursor:pointer;}#mermaid-svg-JOejoFVOBGwy4JpJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JOejoFVOBGwy4JpJ .arrowheadPath{fill:#333333;}#mermaid-svg-JOejoFVOBGwy4JpJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JOejoFVOBGwy4JpJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JOejoFVOBGwy4JpJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JOejoFVOBGwy4JpJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JOejoFVOBGwy4JpJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JOejoFVOBGwy4JpJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster text{fill:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ .cluster span{color:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JOejoFVOBGwy4JpJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JOejoFVOBGwy4JpJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-JOejoFVOBGwy4JpJ .icon-shape,#mermaid-svg-JOejoFVOBGwy4JpJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JOejoFVOBGwy4JpJ .icon-shape p,#mermaid-svg-JOejoFVOBGwy4JpJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JOejoFVOBGwy4JpJ .icon-shape .label rect,#mermaid-svg-JOejoFVOBGwy4JpJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JOejoFVOBGwy4JpJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JOejoFVOBGwy4JpJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JOejoFVOBGwy4JpJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Kubernetes 集群
Training Operator (Deployment)
提交 CR
reconcile
创建 Pod/Service
watch
调度
业务节点
DNS 发现
Pod: worker-0
Service: worker-0

(headless)
Pod: worker-1
Pod: worker-N
API Server + etcd
PyTorchJob Controller
TensorFlowJob Controller
MPIJob Controller
JAXJob Controller
PaddlePaddleJob Controller
通用库

(Pod/Service/Status 管理)
PyTorchJob CR
TensorFlowJob CR
数据科学家

kubectl apply -f pytorchjob.yaml

Training Operator 本身是一个 Deployment,跑在 K8s 控制面。它内部为每种框架注册一个 Controller:PyTorchJob、TensorFlowJob、MPIJob、JAXJob、PaddlePaddleJob 等。所有 Controller 共享一套通用逻辑(Pod 创建、Service 注入、Status 上报),只是各框架的启动命令、环境变量注入策略不同。

2.2 通用 reconcile 流程

无论哪个框架,Controller 的核心 reconcile 逻辑都是:
kubelet Scheduler Controller API Server 用户 kubelet Scheduler Controller API Server 用户 #mermaid-svg-Atlr175KzF9ZRsZv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Atlr175KzF9ZRsZv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Atlr175KzF9ZRsZv .error-icon{fill:#552222;}#mermaid-svg-Atlr175KzF9ZRsZv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Atlr175KzF9ZRsZv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Atlr175KzF9ZRsZv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Atlr175KzF9ZRsZv .marker.cross{stroke:#333333;}#mermaid-svg-Atlr175KzF9ZRsZv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Atlr175KzF9ZRsZv p{margin:0;}#mermaid-svg-Atlr175KzF9ZRsZv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Atlr175KzF9ZRsZv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Atlr175KzF9ZRsZv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Atlr175KzF9ZRsZv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Atlr175KzF9ZRsZv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Atlr175KzF9ZRsZv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Atlr175KzF9ZRsZv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Atlr175KzF9ZRsZv .sequenceNumber{fill:white;}#mermaid-svg-Atlr175KzF9ZRsZv #sequencenumber{fill:#333;}#mermaid-svg-Atlr175KzF9ZRsZv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Atlr175KzF9ZRsZv .messageText{fill:#333;stroke:none;}#mermaid-svg-Atlr175KzF9ZRsZv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Atlr175KzF9ZRsZv .labelText,#mermaid-svg-Atlr175KzF9ZRsZv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Atlr175KzF9ZRsZv .loopText,#mermaid-svg-Atlr175KzF9ZRsZv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Atlr175KzF9ZRsZv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Atlr175KzF9ZRsZv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Atlr175KzF9ZRsZv .noteText,#mermaid-svg-Atlr175KzF9ZRsZv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Atlr175KzF9ZRsZv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Atlr175KzF9ZRsZv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Atlr175KzF9ZRsZv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Atlr175KzF9ZRsZv .actorPopupMenu{position:absolute;}#mermaid-svg-Atlr175KzF9ZRsZv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Atlr175KzF9ZRsZv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Atlr175KzF9ZRsZv .actor-man circle,#mermaid-svg-Atlr175KzF9ZRsZv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Atlr175KzF9ZRsZv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} worker-3 失败 → status 更新 Failed kubectl apply -f pytorchjob.yaml watch 到新 PyTorchJob CR 计算 desired replicas 创建 N 个 Pod + N 个 headless Service 调度 Pod 绑定 Pod → Node 创建容器 注入 WORLD_SIZE/RANK/MASTER_ADDR Pod Running 更新 PyTorchJob.status 监听 Pod 事件 是否触发 restartPolicy? 重启或标记 Job Failed

关键设计点:

  1. headless Service :每个 worker Pod 对应一个 headless Service(同名的 DNS 记录),让其他 worker 可以通过 <pod-name>.<service-name> 找到它。这是 PyTorch init_process_group(backend=nccl)MASTER_ADDR 通常指向的地址。
  2. 环境变量注入 :Operator 在创建 Pod 时自动计算并注入 WORLD_SIZERANKMASTER_ADDRMASTER_PORT,用户 Python 代码直接 os.environ 读取。
  3. 状态机管理 :CR 的 .status.conditions 字段记录 Created → Running → Succeeded/Failed 状态流转。
  4. 重启策略 :单个 worker 失败时按 restartPolicy 决定是否重启整个 Job 或仅重启失败 Pod。

2.3 Training Operator 安装

用 Helm 一行安装:

bash 复制代码
helm repo add kubeflow https://www.kubeflow.org/experimental/training-operator
helm repo update
helm install training-operator kubeflow/training-operator \
  --namespace kubeflow \
  --create-namespace \
  --set webhook.port=8443

安装后检查 CRD 是否注册:

bash 复制代码
$ kubectl get crd | grep kubeflow.org
mpijobs.kubeflow.org
paddlepaddlejobs.kubeflow.org
pytorchjobs.kubeflow.org
tfjobs.kubeflow.org
xgboostjobs.kubeflow.org

三、PyTorchJob CRD 详解

3.1 最简 PyTorchJob

PyTorchJob 是 LLM 训练用得最多的 CRD。一个最小的 16 卡 DDP 训练任务:

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llama3-8b-finetune
  namespace: training
spec:
  pytorchReplicaSpecs:
    Worker:
      replicas: 4                          # 4 个 worker 进程
      restartPolicy: OnFailure             # worker 失败时重启 Pod
      template:
        spec:
          containers:
            - name: pytorch
              image: registry.internal/pytorch:2.3.0-cuda12.1
              command:
                - torchrun
                - --nproc_per_node=4       # 每节点 4 卡(4 节点共 16 卡)
                - --nnodes=4
                - --rdzv_backend=c10d
                - --rdzv_endpoint=llama3-8b-finetune-worker-0:29500
                - train.py
                - --model=meta-llama/Llama-3-8B
                - --output=/checkpoints
              resources:
                limits:
                  nvidia.com/gpu: 4        # 每个 worker 4 张 GPU
                requests:
                  nvidia.com/gpu: 4
              volumeMounts:
                - name: data
                  mountPath: /data
                - name: ckpt
                  mountPath: /checkpoints
          volumes:
            - name: data
              persistentVolumeClaim:
                claimName: training-data
            - name: ckpt
              persistentVolumeClaim:
                claimName: checkpoint-pvc
          nodeSelector:
            nvidia.com/gpu.product: A100-SXM4-40GB
          tolerations:
            - key: nvidia.com/gpu
              operator: Exists

提交后:

bash 复制代码
kubectl apply -f llama3-pytorchjob.yaml
kubectl get pytorchjob -n training
NAME                 STATE       AGE
llama3-8b-finetune   Running     2m

3.2 字段含义详解

pytorchReplicaSpecs 是核心字段,支持两种角色:

角色 含义 典型用法
Master 0 号 worker,特殊角色 早期 PyTorch 用,现代 DDP 已弱化
Worker 所有 worker,等价角色 现代 DDP 只用 Worker

现代 PyTorch 训练只用 Worker,所有 worker 完全对等(rank 0 是约定俗成的 master)。Master 角色为兼容旧代码保留。

每个 ReplicaSpec 包含:

  • replicas:该角色副本数。
  • restartPolicyAlways / OnFailure / Never。生产推荐 OnFailure,失败重试但成功完成不重启。
  • template:PodTemplate,与普通 Pod spec 一致。

3.3 环境变量自动注入

Operator 不需要用户手动写 RANKWORLD_SIZE。它会自动给每个 Pod 注入:

复制代码
WORLD_SIZE = sum(all replicas)
RANK = 该 Pod 在所有 replica 中的序号
MASTER_ADDR = worker-0 的 headless Service DNS
MASTER_PORT = 29500 (默认)

容器内 Python 代码:

python 复制代码
import os
import torch.distributed as dist

rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
master_addr = os.environ["MASTER_ADDR"]

dist.init_process_group(
    backend="nccl",
    init_method=f"tcp://{master_addr}:29500",
    rank=rank,
    world_size=world_size,
)

这种约定大幅减少了训练代码的样板。用户写训练代码时甚至不需要考虑「我在 K8s 里跑」,只要遵循 RANK/WORLD_SIZE/MASTER_ADDR 约定即可。

3.4 完整 CRD 结构

完整的 PyTorchJob CRD 字段(精简):

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: <job-name>
spec:
  # 主动清理策略:Job 完成后多久删除底层 Pod
  runPolicy:
    cleanPodPolicy: Running      # Running / All / None
    ttlSecondsAfterFinished: 86400  # 完成后保留 24 小时
    activeDeadlineSeconds: 86400    # 最长运行 24 小时
    backoffLimit: 6                 # 最多重试 6 次
    schedulingPolicy:
      priorityClass: gpu-high
      queue: training-queue         # Kueue 队列
  pytorchReplicaSpecs:
    Worker:
      replicas: 4
      restartPolicy: OnFailure
      template:
        metadata:
          labels:
            team: llm-research
        spec:
          # ... PodSpec
status:
  conditions:                       # 状态流转记录
    - type: Created
      status: "True"
      lastTransitionTime: "..."
    - type: Running
      status: "True"
      lastTransitionTime: "..."
  replicaStatuses:
    Worker:
      active: 4
      succeeded: 0
      failed: 0

3.5 状态机与条件

Training Operator 通过 .status.conditions 记录 Job 的生命周期:
#mermaid-svg-wra9J7YapNIQbhmb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wra9J7YapNIQbhmb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wra9J7YapNIQbhmb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wra9J7YapNIQbhmb .error-icon{fill:#552222;}#mermaid-svg-wra9J7YapNIQbhmb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wra9J7YapNIQbhmb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wra9J7YapNIQbhmb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wra9J7YapNIQbhmb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wra9J7YapNIQbhmb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wra9J7YapNIQbhmb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wra9J7YapNIQbhmb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wra9J7YapNIQbhmb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wra9J7YapNIQbhmb .marker.cross{stroke:#333333;}#mermaid-svg-wra9J7YapNIQbhmb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wra9J7YapNIQbhmb p{margin:0;}#mermaid-svg-wra9J7YapNIQbhmb defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-wra9J7YapNIQbhmb g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-wra9J7YapNIQbhmb g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-wra9J7YapNIQbhmb g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-wra9J7YapNIQbhmb g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-wra9J7YapNIQbhmb g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-wra9J7YapNIQbhmb .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-wra9J7YapNIQbhmb .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-wra9J7YapNIQbhmb .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-wra9J7YapNIQbhmb .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wra9J7YapNIQbhmb .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-wra9J7YapNIQbhmb .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-wra9J7YapNIQbhmb .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-wra9J7YapNIQbhmb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wra9J7YapNIQbhmb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wra9J7YapNIQbhmb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wra9J7YapNIQbhmb .edgeLabel .label text{fill:#333;}#mermaid-svg-wra9J7YapNIQbhmb .label div .edgeLabel{color:#333;}#mermaid-svg-wra9J7YapNIQbhmb .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-wra9J7YapNIQbhmb .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-wra9J7YapNIQbhmb .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-wra9J7YapNIQbhmb .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-wra9J7YapNIQbhmb .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-wra9J7YapNIQbhmb .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wra9J7YapNIQbhmb .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wra9J7YapNIQbhmb #statediagram-barbEnd{fill:#333333;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wra9J7YapNIQbhmb .cluster-label,#mermaid-svg-wra9J7YapNIQbhmb .nodeLabel{color:#131300;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-wra9J7YapNIQbhmb .note-edge{stroke-dasharray:5;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-note text{fill:black;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram-note .nodeLabel{color:black;}#mermaid-svg-wra9J7YapNIQbhmb .statediagram .edgeLabel{color:red;}#mermaid-svg-wra9J7YapNIQbhmb #dependencyStart,#mermaid-svg-wra9J7YapNIQbhmb #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-wra9J7YapNIQbhmb .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wra9J7YapNIQbhmb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CR 被创建
所有 Pod 起来
镜像拉取失败等致命错误
单 worker 失败+restartPolicy
重启完成
训练正常退出
超出 backoffLimit
cleanPodPolicy 清理
cleanPodPolicy 清理
Created
Running
Failed
Restarting
Succeeded

获取状态详情:

bash 复制代码
$ kubectl describe pytorchjob llama3-8b-finetune -n training
...
Status:
  Conditions:
    Last Transition Time:  2026-07-31T10:00:00Z
    Reason:                PyTorchJobRunning
    Status:                True
    Type:                  Running
  Replica Statuses:
    Worker:
      Active:  4

四、其他框架的 CRD

4.1 MPIJob

MPIJob 用于基于 MPI 的训练(典型如 Megatron-LM 的 Slurm 风格启动)。结构略复杂,多了一个 Launcher 角色:

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: MPIJob
metadata:
  name: megatron-training
spec:
  slotsPerWorker: 8                   # 每个 worker 8 个 slot(GPU)
  runPolicy:
    cleanPodPolicy: Running
  mpiReplicaSpecs:
    Launcher:                         # 启动器,跑 mpirun
      replicas: 1
      template:
        spec:
          containers:
            - name: mpi-launcher
              image: registry.internal/megatron:latest
              command: [/scripts/launch_mpirun.sh]
    Worker:                           # 真正训练节点
      replicas: 4
      template:
        spec:
          containers:
            - name: mpi-worker
              image: registry.internal/megatron:latest
              resources:
                limits:
                  nvidia.com/gpu: 8

Launcher 负责调用 mpirun 通过 SSH 把任务派发到 Worker。Worker 节点通过 OpenSSH Server 接受 Launcher 调度。

4.2 TensorFlowJob

TensorFlowJob 主要为 parameter server 架构设计,有 Chief / Worker / PS 三种角色:

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: TFJob
metadata:
  name: tf-resnet-training
spec:
  tfReplicaSpecs:
    Chief:                            # 主 worker,写 checkpoint
      replicas: 1
      template: ...
    Worker:                           # 普通训练 worker
      replicas: 8
      template: ...
    PS:                               # parameter server
      replicas: 4
      template:                       # 通常用 CPU
        spec:
          containers:
            - name: tensorflow
              image: tensorflow/tensorflow:2.15.0-gpu
              resources:
                limits:
                  cpu: 16
                  memory: 64Gi

随着 TensorFlow 对 CollectiveAllReduceStrategy(与 PyTorch DDP 类似)的成熟,PS 角色用得越来越少。但 TFJob 仍保留它以兼容旧代码。


五、Gang Scheduling:训练任务的命脉

5.1 死锁重现

回到开篇的死锁例子。即便用 PyTorchJob,默认调度器仍然是「先到先得」:
集群(16 GPU 空闲) K8s Scheduler 任务B (8 GPU) 任务A (16 GPU) 集群(16 GPU 空闲) K8s Scheduler 任务B (8 GPU) 任务A (16 GPU) #mermaid-svg-buFGJt9HCTwx94GL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-buFGJt9HCTwx94GL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-buFGJt9HCTwx94GL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-buFGJt9HCTwx94GL .error-icon{fill:#552222;}#mermaid-svg-buFGJt9HCTwx94GL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-buFGJt9HCTwx94GL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-buFGJt9HCTwx94GL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-buFGJt9HCTwx94GL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-buFGJt9HCTwx94GL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-buFGJt9HCTwx94GL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-buFGJt9HCTwx94GL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-buFGJt9HCTwx94GL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-buFGJt9HCTwx94GL .marker.cross{stroke:#333333;}#mermaid-svg-buFGJt9HCTwx94GL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-buFGJt9HCTwx94GL p{margin:0;}#mermaid-svg-buFGJt9HCTwx94GL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-buFGJt9HCTwx94GL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-buFGJt9HCTwx94GL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-buFGJt9HCTwx94GL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-buFGJt9HCTwx94GL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-buFGJt9HCTwx94GL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-buFGJt9HCTwx94GL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-buFGJt9HCTwx94GL .sequenceNumber{fill:white;}#mermaid-svg-buFGJt9HCTwx94GL #sequencenumber{fill:#333;}#mermaid-svg-buFGJt9HCTwx94GL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-buFGJt9HCTwx94GL .messageText{fill:#333;stroke:none;}#mermaid-svg-buFGJt9HCTwx94GL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-buFGJt9HCTwx94GL .labelText,#mermaid-svg-buFGJt9HCTwx94GL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-buFGJt9HCTwx94GL .loopText,#mermaid-svg-buFGJt9HCTwx94GL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-buFGJt9HCTwx94GL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-buFGJt9HCTwx94GL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-buFGJt9HCTwx94GL .noteText,#mermaid-svg-buFGJt9HCTwx94GL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-buFGJt9HCTwx94GL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-buFGJt9HCTwx94GL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-buFGJt9HCTwx94GL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-buFGJt9HCTwx94GL .actorPopupMenu{position:absolute;}#mermaid-svg-buFGJt9HCTwx94GL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-buFGJt9HCTwx94GL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-buFGJt9HCTwx94GL .actor-man circle,#mermaid-svg-buFGJt9HCTwx94GL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-buFGJt9HCTwx94GL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 剩余 6 GPU 剩余 0, 任务A 还差 6, 任务B 还差 2 两个任务都卡死! 申请 16 申请 8 调度任务A 10 个 Pod 调度任务B 6 个 Pod

默认调度器看到 Pod 就尽量调度,没有「同 Job 的 Pod 一起」的概念。死锁在所难免。

5.2 Gang Scheduling 原理

Gang Scheduling 的核心思想:把一组 Pod 当作一个调度单元(PodGroup),要么全部调度成功,要么都不调度 。K8s 上实现 Gang Scheduling 的主流方案是 Volcano(前身 kube-batch)。

Volcano 引入了一个新的 CRD PodGroup,并自己实现了一个调度器。PyTorchJob 创建 Pod 时同时创建一个 PodGroup,声明这些 Pod 必须成组调度:
#mermaid-svg-4YHPAfmUGDFZ7a7k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4YHPAfmUGDFZ7a7k .error-icon{fill:#552222;}#mermaid-svg-4YHPAfmUGDFZ7a7k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4YHPAfmUGDFZ7a7k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .marker.cross{stroke:#333333;}#mermaid-svg-4YHPAfmUGDFZ7a7k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4YHPAfmUGDFZ7a7k p{margin:0;}#mermaid-svg-4YHPAfmUGDFZ7a7k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster-label text{fill:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster-label span{color:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster-label span p{background-color:transparent;}#mermaid-svg-4YHPAfmUGDFZ7a7k .label text,#mermaid-svg-4YHPAfmUGDFZ7a7k span{fill:#333;color:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .node rect,#mermaid-svg-4YHPAfmUGDFZ7a7k .node circle,#mermaid-svg-4YHPAfmUGDFZ7a7k .node ellipse,#mermaid-svg-4YHPAfmUGDFZ7a7k .node polygon,#mermaid-svg-4YHPAfmUGDFZ7a7k .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .rough-node .label text,#mermaid-svg-4YHPAfmUGDFZ7a7k .node .label text,#mermaid-svg-4YHPAfmUGDFZ7a7k .image-shape .label,#mermaid-svg-4YHPAfmUGDFZ7a7k .icon-shape .label{text-anchor:middle;}#mermaid-svg-4YHPAfmUGDFZ7a7k .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .rough-node .label,#mermaid-svg-4YHPAfmUGDFZ7a7k .node .label,#mermaid-svg-4YHPAfmUGDFZ7a7k .image-shape .label,#mermaid-svg-4YHPAfmUGDFZ7a7k .icon-shape .label{text-align:center;}#mermaid-svg-4YHPAfmUGDFZ7a7k .node.clickable{cursor:pointer;}#mermaid-svg-4YHPAfmUGDFZ7a7k .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .arrowheadPath{fill:#333333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4YHPAfmUGDFZ7a7k .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4YHPAfmUGDFZ7a7k .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4YHPAfmUGDFZ7a7k .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster text{fill:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k .cluster span{color:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4YHPAfmUGDFZ7a7k .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4YHPAfmUGDFZ7a7k rect.text{fill:none;stroke-width:0;}#mermaid-svg-4YHPAfmUGDFZ7a7k .icon-shape,#mermaid-svg-4YHPAfmUGDFZ7a7k .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4YHPAfmUGDFZ7a7k .icon-shape p,#mermaid-svg-4YHPAfmUGDFZ7a7k .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4YHPAfmUGDFZ7a7k .icon-shape .label rect,#mermaid-svg-4YHPAfmUGDFZ7a7k .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4YHPAfmUGDFZ7a7k .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4YHPAfmUGDFZ7a7k .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4YHPAfmUGDFZ7a7k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调度决策
Volcano 调度链路
创建时同步创建


PyTorchJob (16 worker)
PodGroup minAvailable=16
Volcano Scheduler
queue: training
检查 queue 中所有 PodGroup
minAvailable 需求 ≤ 可用资源?
是 → 成组调度
否 → 全部等待

5.3 启用 Volcano 调度

要让 PyTorchJob 用 Volcano 调度,配置:

yaml 复制代码
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llama3-finetune-gang
spec:
  schedulingPolicy:
    schedulerName: volcano            # 用 Volcano 调度器
    minAvailable: 4                   # 必须 4 个 worker 同时调度
  pytorchReplicaSpecs:
    Worker:
      replicas: 4
      template:
        spec:
          schedulerName: volcano      # Pod 也指定
          containers:
            - name: pytorch
              resources:
                limits:
                  nvidia.com/gpu: 4

minAvailable 是 Gang Scheduling 的关键参数:

  • minAvailable: 4 表示 4 个 Pod 都能调度时才动手。
  • 可以小于 replicas(如 minAvailable=3, replicas=4)允许部分冗余。

Volcano 还提供:

  • Queue(队列):按租户/团队划分资源池,超配时按优先级排队。
  • Preempt(抢占):高优 PodGroup 可抢占低优 PodGroup 的资源。
  • Topology Constraint:约束 Pod 调度的拓扑分布(如一个 worker 必须在同一节点)。

5.4 Volcano Queue 与优先级

yaml 复制代码
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: training-queue
spec:
  weight: 3                            # 该队列权重
  reclaimable: true                    # 允许被抢占
  capability:
    cpu: 1000
    memory: 4Ti
    nvidia.com/gpu: 64                 # 队列总配额
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
  name: llama3-job-pg
spec:
  minMember: 4                         # 4 个 Pod 才能启动
  queue: training-queue
  priorityClassName: gpu-high
  minResources:
    nvidia.com/gpu: 16                 # 至少需要 16 GPU

队列机制是规模化 AIaaS 平台的资源公平分配基础:

  • 不同租户/团队有独立 Queue,按 weight 分集群资源。
  • 单个 Queue 内多个 PodGroup 按 priority 排队。
  • 跨 Queue 抢占保证高优 Queue 的 SLA。

5.5 Kueue:新一代批调度器

除 Volcano 外,K8s SIG 又推出了 Kueue,定位更轻量,专为批处理任务设计:

维度 Volcano Kueue
定位 全功能批调度器 排队 + 准入控制器
是否替换默认调度器 是(自带调度器) 否(用默认调度器)
Gang Scheduling 原生支持 通过 COSI/集成
与 K8s 集成 重,自带 RBAC/CRD 轻,更接近 K8s 原生
推荐场景 大规模多租户 K8s 1.27+ 上的现代部署

Kueue 的核心是 ClusterQueueLocalQueue 两个 CRD,配合默认调度器实现公平排队和配额借用,不需要替换调度器。Kubeflow Training Operator v1.7+ 原生支持 Kueue。

yaml 复制代码
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
      flavors:
        - name: gpu-a100
          resources:
            - name: nvidia.com/gpu
              nominalQuota: 64
            - name: cpu
              nominalQuota: 2048

六、训练日志与状态监控

6.1 Pod 日志查看

每个 worker 都是一个 Pod,独立查日志:

bash 复制代码
# 列出所有 worker
kubectl get pods -n training -l job-name=llama3-8b-finetune
NAME                          READY   STATUS    RESTARTS
llama3-8b-finetune-worker-0   1/1     Running   0
llama3-8b-finetune-worker-1   1/1     Running   0
llama3-8b-finetune-worker-2   1/1     Running   0
llama3-8b-finetune-worker-3   1/1     Running   0

# 单个 worker 日志
kubectl logs -n training llama3-8b-finetune-worker-0 -f

# 同时看所有 worker(用 stern 之类的工具)
stern -n training llama3-8b-finetune-worker --tail 100

6.2 训练指标采集

训练任务的 metrics 不在 Prometheus 默认抓取范围,需要业务侧主动暴露:

  • PyTorch Lightning / HuggingFace Trainer:内置 TensorBoard / W&B / MLflow 集成,写到 PVC 或外部服务。
  • 自定义 metrics exporter :训练脚本里 prometheus_client.start_http_server(8000),由 Prometheus 抓取。

典型的训练指标暴露片段:

python 复制代码
from prometheus_client import Gauge, start_http_server

# 定义指标
loss_gauge = Gauge('training_loss', 'Current training loss', ['step'])
lr_gauge = Gauge('training_learning_rate', 'Current learning rate')
gpu_mem_gauge = Gauge('training_gpu_memory_allocated_bytes', 'GPU memory allocated')

# 启动 metrics server
start_http_server(8000)

# 训练循环中
for step, batch in enumerate(dataloader):
    loss = train_step(batch)
    loss_gauge.labels(step=step).set(loss.item())
    lr_gauge.set(get_lr())
    gpu_mem_gauge.set(torch.cuda.memory_allocated())

Prometheus 抓取配置(针对训练 Pod):

yaml 复制代码
- job_name: 'training-jobs'
  kubernetes_sd_configs:
    - role: pod
  relabel_configs:
    - source_labels: [__meta_kubernetes_pod_label_job-name]
      regex: .+
      action: keep
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
      action: keep
      regex: true
  metrics_path: /metrics

6.3 GPU 利用率监控

每个 worker Pod 的 GPU 利用率通过 DCGM Exporter 采集:

promql 复制代码
# 按 Pod 维度查 GPU 利用率
DCGM_FI_DEV_GPU_UTIL{pod=~"llama3-8b-finetune-worker.*"}

# 按 Job 聚合
avg(DCGM_FI_DEV_GPU_UTIL{pod=~"llama3.*"}) by (pod)

# 整个集群的 GPU 利用率分布
histogram_quantile(0.95, sum(rate(DCGM_FI_DEV_GPU_UTIL_bucket[5m])) by (le))

如果训练任务 GPU 利用率长期低于 70%,通常是数据 pipeline 没跟上或通信瓶颈,需要排查 DataLoader num_workers / NVLink 拓扑。

6.4 Checkpoint 管理

分布式训练必须支持 checkpoint 续训。一个生产可用的 checkpoint 策略:

yaml 复制代码
# 在训练脚本中(伪代码)
def save_checkpoint(step, model, optimizer, epoch):
    ckpt_path = f"/checkpoints/step-{step}.pt"
    # DDP 下只有 rank 0 写
    if dist.get_rank() == 0:
        torch.save({
            'step': step,
            'epoch': epoch,
            'model_state': model.state_dict(),
            'optimizer_state': optimizer.state_dict(),
            'rng_state': torch.get_rng_state(),
        }, ckpt_path)
    dist.barrier()                     # 所有 rank 同步

存储建议:

  • checkpoint 写到 PVC(ReadWriteMany,多 Pod 共享)。
  • 长期归档上传到对象存储(S3/OSS),减小 PVC 压力。
  • 保留最近 3-5 个 checkpoint + 每隔 N step 一个长期 checkpoint。

七、生产落地:从单任务到批调度

7.1 任务生命周期管理

#mermaid-svg-5KICqbdJFlENGhBV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5KICqbdJFlENGhBV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5KICqbdJFlENGhBV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5KICqbdJFlENGhBV .error-icon{fill:#552222;}#mermaid-svg-5KICqbdJFlENGhBV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5KICqbdJFlENGhBV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5KICqbdJFlENGhBV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5KICqbdJFlENGhBV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5KICqbdJFlENGhBV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5KICqbdJFlENGhBV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5KICqbdJFlENGhBV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5KICqbdJFlENGhBV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5KICqbdJFlENGhBV .marker.cross{stroke:#333333;}#mermaid-svg-5KICqbdJFlENGhBV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5KICqbdJFlENGhBV p{margin:0;}#mermaid-svg-5KICqbdJFlENGhBV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5KICqbdJFlENGhBV .cluster-label text{fill:#333;}#mermaid-svg-5KICqbdJFlENGhBV .cluster-label span{color:#333;}#mermaid-svg-5KICqbdJFlENGhBV .cluster-label span p{background-color:transparent;}#mermaid-svg-5KICqbdJFlENGhBV .label text,#mermaid-svg-5KICqbdJFlENGhBV span{fill:#333;color:#333;}#mermaid-svg-5KICqbdJFlENGhBV .node rect,#mermaid-svg-5KICqbdJFlENGhBV .node circle,#mermaid-svg-5KICqbdJFlENGhBV .node ellipse,#mermaid-svg-5KICqbdJFlENGhBV .node polygon,#mermaid-svg-5KICqbdJFlENGhBV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5KICqbdJFlENGhBV .rough-node .label text,#mermaid-svg-5KICqbdJFlENGhBV .node .label text,#mermaid-svg-5KICqbdJFlENGhBV .image-shape .label,#mermaid-svg-5KICqbdJFlENGhBV .icon-shape .label{text-anchor:middle;}#mermaid-svg-5KICqbdJFlENGhBV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5KICqbdJFlENGhBV .rough-node .label,#mermaid-svg-5KICqbdJFlENGhBV .node .label,#mermaid-svg-5KICqbdJFlENGhBV .image-shape .label,#mermaid-svg-5KICqbdJFlENGhBV .icon-shape .label{text-align:center;}#mermaid-svg-5KICqbdJFlENGhBV .node.clickable{cursor:pointer;}#mermaid-svg-5KICqbdJFlENGhBV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5KICqbdJFlENGhBV .arrowheadPath{fill:#333333;}#mermaid-svg-5KICqbdJFlENGhBV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5KICqbdJFlENGhBV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5KICqbdJFlENGhBV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5KICqbdJFlENGhBV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5KICqbdJFlENGhBV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5KICqbdJFlENGhBV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5KICqbdJFlENGhBV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5KICqbdJFlENGhBV .cluster text{fill:#333;}#mermaid-svg-5KICqbdJFlENGhBV .cluster span{color:#333;}#mermaid-svg-5KICqbdJFlENGhBV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5KICqbdJFlENGhBV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5KICqbdJFlENGhBV rect.text{fill:none;stroke-width:0;}#mermaid-svg-5KICqbdJFlENGhBV .icon-shape,#mermaid-svg-5KICqbdJFlENGhBV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5KICqbdJFlENGhBV .icon-shape p,#mermaid-svg-5KICqbdJFlENGhBV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5KICqbdJFlENGhBV .icon-shape .label rect,#mermaid-svg-5KICqbdJFlENGhBV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5KICqbdJFlENGhBV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5KICqbdJFlENGhBV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5KICqbdJFlENGhBV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否




否, 重试次数未达上限
否, 重试上限
用户提交

PyTorchJob YAML
Kueue 准入控制
队列配额

是否够?
排队等待

LocalQueue
Training Operator

创建 Pod
Volcano

Gang Scheduling
minAvailable

满足?
挂起

等待资源
所有 Pod 启动
训练循环
正常完成?
status: Succeeded

cleanPodPolicy
重启 worker
status: Failed
告警 + 保留日志

7.2 多租户任务队列

生产 AIaaS 平台通常按租户划分 Queue,按 SLA 配置优先级:

yaml 复制代码
# 租户 A: 高级用户,优先级高,配额大
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: tenant-a-premium
spec:
  cohort: gpu-cohort
  resourceGroups:
    - flavors:
        - name: a100-flavor
          resources:
            - name: nvidia.com/gpu
              nominalQuota: 32                  # 保证配额
              borrowingLimit: 16                # 可借 16
---
# 租户 B: 免费用户,优先级低,配额小
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: tenant-b-free
spec:
  cohort: gpu-cohort
  resourceGroups:
    - flavors:
        - name: a100-flavor
          resources:
            - name: nvidia.com/gpu
              nominalQuota: 4                   # 保证 4 GPU
              borrowingLimit: 0                 # 不可借

cohort 让多个 Queue 在同一资源池里互相借调。租户 A 高峰期可以借走租户 B 没用的 GPU,反之不行(borrowingLimit: 0)。

7.3 训练任务 CI/CD

把训练任务纳入 CI/CD:
#mermaid-svg-PTMwdvGPgfLu6dHS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PTMwdvGPgfLu6dHS .error-icon{fill:#552222;}#mermaid-svg-PTMwdvGPgfLu6dHS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PTMwdvGPgfLu6dHS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PTMwdvGPgfLu6dHS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PTMwdvGPgfLu6dHS .marker.cross{stroke:#333333;}#mermaid-svg-PTMwdvGPgfLu6dHS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PTMwdvGPgfLu6dHS p{margin:0;}#mermaid-svg-PTMwdvGPgfLu6dHS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster-label text{fill:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster-label span{color:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster-label span p{background-color:transparent;}#mermaid-svg-PTMwdvGPgfLu6dHS .label text,#mermaid-svg-PTMwdvGPgfLu6dHS span{fill:#333;color:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS .node rect,#mermaid-svg-PTMwdvGPgfLu6dHS .node circle,#mermaid-svg-PTMwdvGPgfLu6dHS .node ellipse,#mermaid-svg-PTMwdvGPgfLu6dHS .node polygon,#mermaid-svg-PTMwdvGPgfLu6dHS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PTMwdvGPgfLu6dHS .rough-node .label text,#mermaid-svg-PTMwdvGPgfLu6dHS .node .label text,#mermaid-svg-PTMwdvGPgfLu6dHS .image-shape .label,#mermaid-svg-PTMwdvGPgfLu6dHS .icon-shape .label{text-anchor:middle;}#mermaid-svg-PTMwdvGPgfLu6dHS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PTMwdvGPgfLu6dHS .rough-node .label,#mermaid-svg-PTMwdvGPgfLu6dHS .node .label,#mermaid-svg-PTMwdvGPgfLu6dHS .image-shape .label,#mermaid-svg-PTMwdvGPgfLu6dHS .icon-shape .label{text-align:center;}#mermaid-svg-PTMwdvGPgfLu6dHS .node.clickable{cursor:pointer;}#mermaid-svg-PTMwdvGPgfLu6dHS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PTMwdvGPgfLu6dHS .arrowheadPath{fill:#333333;}#mermaid-svg-PTMwdvGPgfLu6dHS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PTMwdvGPgfLu6dHS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PTMwdvGPgfLu6dHS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PTMwdvGPgfLu6dHS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PTMwdvGPgfLu6dHS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PTMwdvGPgfLu6dHS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster text{fill:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS .cluster span{color:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PTMwdvGPgfLu6dHS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PTMwdvGPgfLu6dHS rect.text{fill:none;stroke-width:0;}#mermaid-svg-PTMwdvGPgfLu6dHS .icon-shape,#mermaid-svg-PTMwdvGPgfLu6dHS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PTMwdvGPgfLu6dHS .icon-shape p,#mermaid-svg-PTMwdvGPgfLu6dHS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PTMwdvGPgfLu6dHS .icon-shape .label rect,#mermaid-svg-PTMwdvGPgfLu6dHS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PTMwdvGPgfLu6dHS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PTMwdvGPgfLu6dHS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PTMwdvGPgfLu6dHS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

Git 仓库

(训练代码+超参)
CI: lint + 单测
构建镜像
推送到 Registry
渲染 PyTorchJob YAML

(镜像 tag 注入)
kubectl apply
Argo Workflows

编排多步
训练任务
指标自动评估
达标?
自动注册到

Model Registry
告警人工介入

这里 Argo Workflows 充当训练流水线的总编排,PyTorchJob 是其中一步。这是模块六模型流水线的内容。

7.4 常见故障与排查

现象 原因 排查
PyTorchJob 长期 Created 不 Running Pod Pending,资源不够 看 Pod events,是否 FailedScheduling
Worker 起来但 init_process_group 卡住 部分 worker 没到,DNS 没解析 kubectl exec 进 Pod 试 ping worker-0
训练几十步后 NCCL timeout NVLink/InfiniBand 故障,或跨 NUMA 看 dmesg,nvidia-smi
Worker OOM batch size 太大或显存配额不够 减小 batch,开启 ZeRO
Worker 突然退出码 137 OOM Killer 看 dmesg,调内存 limit
Worker 退出码 143 收到 SIGTERM,被驱逐 看 events,是否被优先级抢占
Volcano 不 Gang Scheduling schedulerName 没设对 看 Pod.schedulerName 是否为 volcano

八、与其他方案的对比

8.1 Training Operator vs Ray Train

维度 Training Operator Ray Train
定位 K8s 原生训练 CRD 通用分布式计算框架
调度 K8s 调度器 + Volcano Ray 自有调度器
适合 标准 PyTorch/TF 分布式 异构任务、超参搜索
弹性 有限(需 PyTorch Elastic) 原生支持 worker 增减
学习成本 低(K8s 语义) 中(Ray 概念多)
多语言 主要 Python Python 原生,Java/C++ 部分

详见下一篇(Ray 与 KubeRay)。

8.2 Training Operator vs 自研 Operator

部分大厂选择自研 Operator,主要差异:

  • 自研优势:可以深度定制启动模板、状态机、资源模型。
  • Training Operator 优势:成熟、社区活跃、与 Kubeflow 生态集成。
  • 折中:基于 Training Operator 上层做 webhook 扩展(注入企业特有逻辑),如审计、配额、计费 hook。

对绝大多数公司,直接用 Training Operator + Volcano/Kueue 是最优解。


本篇小结

主题 核心结论
训练任务的特殊性 all-or-nothing 语义、worker 间通信、checkpoint 续训
Operator 模式 声明式 CR + 控制循环,描述「我要什么」而非「怎么做」
PyTorchJob 用 Worker 角色定义对等 worker,Operator 自动注入 RANK/WORLD_SIZE/MASTER_ADDR
环境变量约定 用户代码读 RANK/WORLD_SIZE/MASTER_ADDR,与 K8s 解耦
Gang Scheduling 一组 Pod 要么全调度要么全不调度,避免死锁
Volcano 实现完整 Gang Scheduling + Queue + 抢占,重型多租户首选
Kueue K8s 原生排队系统,轻量,新项目推荐
日志/监控 Pod 级独立日志,prometheus_client 暴露训练指标
任务队列 ClusterQueue + LocalQueue,按租户/SLA 划分配额
选型 优先 Training Operator + Kueue,自研仅在大规模定制需求

核心心智模型:训练任务 = 一组必须同时启动的 Pod + 一个能识别这个语义的调度器。Operator 解决「怎么定义」,Gang Scheduling 解决「怎么调度」。


下篇预告

第 31 篇:Ray与KubeRay-弹性分布式计算框架

Training Operator 适合「标准」分布式训练,但有些场景需要更灵活的分布式计算------比如超参搜索(每个 worker 跑不同超参)、强化学习(多 Agent 并行)、训练 + 推理混合。Ray 是这类场景的事实标准。下一篇我们讲 Ray 与 KubeRay,看 Ray 的 Head/Worker 架构、Ray Train 与 Ray Serve,以及 KubeRay 如何在 K8s 上管理 Ray 集群。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
fthux1 小时前
装闭 RenoPit 源码解析(14):Demo模式、健康检查与Docker部署
人工智能·ai·开源·github·open source·renopit
IT_陈寒1 小时前
为什么我的JavaScript闭包总是漏掉那个变量?
前端·人工智能·后端
蜘蛛小助理2 小时前
生鲜冷链批次效期与多仓调拨损耗管控|低代码搭建生鲜供应链台账
人工智能·低代码·ai·erp·多维表格·蜘蛛表格·生鲜
ASKED_20192 小时前
从 ASR+LLM+TTS 到 GPT‑Live:解读语音智能新范式
人工智能·gpt·语音识别
DS随心转APP2 小时前
AI生成的word怎么下载?AI导出鸭技术架构深度测评
人工智能·ai·架构·word·deepseek·ai导出鸭
9i编程2 小时前
4. AI编写的SKILL,坑我一一试过,这次我自己改写:换个工具,照样不按SKILL写文档
人工智能·openai·ai编程
zzzzzz3102 小时前
AI 助手最危险的不是报错,而是“看起来成功”:我给工作流加了四道保险
人工智能·python
秦先生在广东2 小时前
用 AI Agents 搭建自动化 Niche 简报,实现每月 $500 被动收入——拆解与验证
人工智能
Fnetlink12 小时前
FNET 云网安 260819
网络·人工智能·安全·网络安全
zzzzzz3102 小时前
一篇文章发五个平台:我用 TRAE Work 搭了条内容分发流水线,2 小时变 20 分钟
人工智能·markdown