【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 进程,每个进程:
- 初始化进程组(
torch.distributed.init_process_group),通过 rendezvous 互相发现。 - 等所有 rank 到齐,初始化 NCCL 通信。
- 进入训练循环,每个 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_SIZE、RANK、MASTER_ADDR 等环境变量;要管理 rendezvous endpoint;要做 checkpoint 续训。这些都需要一个 领域专用控制器。
1.3 Operator 模式:声明式 + 控制循环
Kubeflow Training Operator 采用 K8s 的 Operator 模式:
- 用户声明一个 CR(Custom Resource),描述训练任务期望的样子。
- Operator(一个 Controller 进程)watch 这类 CR。
- Operator 根据期望状态创建/更新 Pod、Service、ConfigMap。
- 不断 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
关键设计点:
- headless Service :每个 worker Pod 对应一个 headless Service(同名的 DNS 记录),让其他 worker 可以通过
<pod-name>.<service-name>找到它。这是 PyTorchinit_process_group(backend=nccl)的MASTER_ADDR通常指向的地址。 - 环境变量注入 :Operator 在创建 Pod 时自动计算并注入
WORLD_SIZE、RANK、MASTER_ADDR、MASTER_PORT,用户 Python 代码直接os.environ读取。 - 状态机管理 :CR 的
.status.conditions字段记录 Created → Running → Succeeded/Failed 状态流转。 - 重启策略 :单个 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:该角色副本数。
- restartPolicy :
Always/OnFailure/Never。生产推荐OnFailure,失败重试但成功完成不重启。 - template:PodTemplate,与普通 Pod spec 一致。
3.3 环境变量自动注入
Operator 不需要用户手动写 RANK、WORLD_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 的核心是 ClusterQueue 和 LocalQueue 两个 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 解决「怎么调度」。
下篇预告
Training Operator 适合「标准」分布式训练,但有些场景需要更灵活的分布式计算------比如超参搜索(每个 worker 跑不同超参)、强化学习(多 Agent 并行)、训练 + 推理混合。Ray 是这类场景的事实标准。下一篇我们讲 Ray 与 KubeRay,看 Ray 的 Head/Worker 架构、Ray Train 与 Ray Serve,以及 KubeRay 如何在 K8s 上管理 Ray 集群。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。