做分布式训练的兄弟,大概率都有过这种经历:部门里五六个人同时提交训练任务,每个人都要好几块GPU。负责集群的你一打开看板,好家伙,大部分卡被一个任务占着,其他任务要么排队排队,要么起来了但一直报OOM。说白了就是资源争抢。咱们今天就聊聊怎么用Kubeflow把这堆破事理顺。

一、现实里的痛:训练任务挤成一锅粥

分布式训练跟普通程序不一样,它一个任务往往要同时起好几个进程。比如TensorFlow的PS架构,要1个参数服务器加多个Worker;PyTorch也会起多个Worker。这些进程在Kubernetes里就是一个个Pod。问题来了:这些Pod本来应该一起调度,但Kubernetes默认调度器只看单个Pod,根本不关心“这个作业是不是一个整体”。如果集群资源只够拉起其中几个Pod,那这几个Pod就会先跑起来,占着资源,剩下的Pod就一直Pending。这就相当于四个人约好坐同一辆车,结果三个人已经坐进去了,车却因为缺一个人开不走,坐在车里的人还把车门堵住,后面的人也没法上别的车。整个训练任务卡死,资源却被白白占用,别人的任务也进不来,越积越多。

Kubeflow正好是解决这类问题的平台。它跑在Kubernetes上,专门管机器学习训练、服务、超参调优这些活儿。要会用它,得先知道它到底靠什么干活。

二、Kubeflow到底是个啥

Kubeflow不是一个单独的大软件,它是一堆组件拼起来的。最核心的是Training Operator,它负责把你提交的训练任务转换成Kubernetes能认的“自定义资源”。你用TensorFlow写代码,就用TFJob;用PyTorch,用PyTorchJob。每个Job会管理一组Pod,确保它们按规则启动、运行、重启。

2.1 核心组件

  • Training Operator:所有分布式训练任务的保姆。
  • Katib:自动帮你调超参数。
  • Pipelines:把整个训练流程串成流水线。
  • Kube-batch / Volcano:这才是咱们今天的主角,管调度的。

2.2 分布式训练任务长啥样

用TFJob举个例子,一个标准的训练任务长这样:

# 技术栈:Shell (kubectl)
# 创建一个TFJob,包含4个Worker,每个Worker要1块GPU
kubectl apply -f - <<EOF
apiVersion: kubeflow.org/v1
kind: TFJob
metadata:
  name: example-tfjob
  namespace: default
spec:
  tfReplicaSpecs:
    Worker:
      replicas: 4
      restartPolicy: OnFailure
      template:
        spec:
          containers:
          - name: tensorflow
            image: tensorflow/tensorflow:2.12.0-gpu
            command: ["python", "/app/train.py"]
            resources:
              requests:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
              limits:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
EOF

这个作业一共要4个Worker Pod,每个都独占一块GPU。如果集群只有3块空闲,这三个Pod会先运行,第四个就一直Pending,这就是我们说的死锁。下面的优化例子,都围绕怎么避免这种尴尬。

三、第一个优化办法:给每个团队圈好地

资源争抢最直接的原因是没有“边界”。我们可以用Kubernetes原生机制,给不同团队分配命名空间,再在命名空间里设资源上限,这样谁也不能超吃。

3.1 ResourceQuota 怎么用

ResourceQuota就像小区里的车位划区,每个团队只能停自己的车位,总共就那么多。比如金融组的训练集群,总共16核CPU、32G内存、8块GPU,我们就这么设:

# 技术栈:Shell (kubectl)
# 先建个属于金融团队的名词空间分账
kubectl create namespace finance-group

# 设总账本:整个命名空间最多用这么多资源
kubectl apply -f - <<EOF
apiVersion: v1
kind: ResourceQuota
metadata:
  name: finance-quota
  namespace: finance-group
spec:
  hard:
    requests.cpu: 16
    requests.memory: 32Gi
    requests.nvidia.com/gpu: 8
    limits.cpu: 20
    limits.memory: 40Gi
EOF

这样金融组一共只能申请8块GPU,就算单个任务想申请100块,也会被拒绝。

3.2 LimitRange 防止“胖墩”任务

ResourceQuota管的是整个命名空间的总量,但挡不住一个任务在里面狮子大开口。LimitRange就是给每个Pod设“尺寸”限制,比如最小要多少、最大能吃多少。这能防止有人把16核CPU全部写在同一个Pod里,导致其他任务没法调度。

# 技术栈:Shell (kubectl)
# 给金融团队的命名空间里的每个Pod定下资源上下限
kubectl apply -f - <<EOF
apiVersion: v1
kind: LimitRange
metadata:
  name: finance-pod-limit
  namespace: finance-group
spec:
  limits:
  - max:
      cpu: "4"
      memory: 8Gi
    min:
      cpu: "200m"
      memory: 256Mi
    default:
      cpu: "1"
      memory: 1Gi
    defaultRequest:
      cpu: "500m"
      memory: 512Mi
    type: Container
EOF

这样任何一个Pod最大的CPU也只能要4核,内存最多8G,想一个人霸占所有资源?想都别想。

四、第二个优化办法:让整组任务一起吃上饭

光靠分地盘还有漏洞:就算每个团队有8块GPU,如果4个Worker都要1块,而集群只剩3块了,那还是会出现死锁。这时需要“Gang Scheduling”。这个词听起来高级,意思很简单:要么所有Pod一起调度成功,要么一个都别启动。

4.1 什么叫Gang Scheduling

把训练任务看成一顿饭,所有Pod是同桌吃饭的人。座位不够?那大家都别坐下,等人齐了再一起上菜。这样就不会有人先占座导致别人没办法。Kubernetes社区里最有名的两个调度器,一个是老牌的Kube-batch,一个是它的继任者Volcano。Kubeflow可以配置使用Volcano。

4.2 用Volcano调度器干活

要让Kubeflow任务走Volcano,先得装上Volcano。用Helm装最省事:

# 技术栈:Shell (kubectl)
# 用Helm把Volcano调度器装进集群
helm install volcano volcano/volcano --namespace volcano-system --create-namespace

装完之后,在TFJob的Pod模板里指定调度器名称:

# 技术栈:Shell (kubectl)
# 创建TFJob,并通过schedulerName参数让Volcano接管调度
kubectl apply -f - <<EOF
apiVersion: kubeflow.org/v1
kind: TFJob
metadata:
  name: example-tfjob-gang
  namespace: finance-group
spec:
  tfReplicaSpecs:
    Worker:
      replicas: 4
      restartPolicy: OnFailure
      template:
        spec:
          schedulerName: volcano
          containers:
          - name: tensorflow
            image: tensorflow/tensorflow:2.12.0-gpu
            command: ["python", "/app/train.py"]
            resources:
              requests:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
              limits:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
EOF

Volcano看到这个作业需要4个Worker,它会先把资源攒起来,如果凑不齐,这4个Pod谁也别想启动。表面上看起来就是大家一起挂Pending,但不会出现“部分运行、部分卡死”的情况。一旦有别的任务结束腾出位置,Volcano会把它们一次性全部放进去,训练立刻开始。

五、第三个优化办法:重要的任务先走

就算有配额,有Gang调度,不同任务的紧急程度还不一样。比如线上正在跑的用户推荐模型要立刻上线,跟一个实验性的小试验抢资源,那肯定重要的大爷优先。这个通过PriorityClass实现。

5.1 PriorityClass 怎么设

PriorityClass就像给任务发“VIP通行证”,数字越大,越优先。我们定义一个高优先级的类:

# 技术栈:Shell (kubectl)
# 定义高优先级类别,value越大越优先
kubectl apply -f - <<EOF
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-training
value: 1000000
globalDefault: false
description: "线上重要训练任务"
EOF

然后在训练任务里引用这个优先级。比如金融团队的高价值模型训练:

# 技术栈:Shell (kubectl)
# 创建TFJob,并指定priorityClassName为高优先级
kubectl apply -f - <<EOF
apiVersion: kubeflow.org/v1
kind: TFJob
metadata:
  name: highprio-fraud-model
  namespace: finance-group
spec:
  tfReplicaSpecs:
    Worker:
      replicas: 4
      restartPolicy: OnFailure
      template:
        spec:
          priorityClassName: high-priority-training
          schedulerName: volcano
          containers:
          - name: tensorflow
            image: tensorflow/tensorflow:2.12.0-gpu
            command: ["python", "/app/train.py"]
            resources:
              requests:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
              limits:
                nvidia.com/gpu: 1
                cpu: "2"
                memory: 4Gi
EOF

加上这行之后,Kubernetes会尝试先把低优先级任务占用的资源腾出来,给高优先级任务用。低优先级任务会被抢占,也就是被干掉然后重新排队。这个能力是调度层面的,Kubeflow作为上层平台,只要修改任务清单就行,非常方便。

5.2 配合Kubeflow的调度器

Kubeflow的Training Operator本身并不做调度,它只负责管理任务的声明。到底谁先跑,是Kubernetes调度器说了算。所以你可以把Volcano、PriorityClass、ResourceQuota这些组合起来。比如高优先级的任务用Volcano凑齐Pod,同时PriorityClass又确保它插队。这样一套组合拳,基本把资源争抢的窟窿都堵上了。

六、实际应用场景分析

这套优化在几个场景特别明显。

第一个是多个团队共用GPU集群。比如车联网公司里,感知组每天跑目标检测训练,地图组跑高精地图模型。两边都对时间敏感,但没有人愿意等。通过命名空间+ResourceQuota,把物理资源切块,各自调度自己那份,互不干扰。

第二个是训练任务弹性提交。算法工程师白天把任务交上去,晚上想看结果,但集群上还有别的任务在跑。如果用了Gang Scheduling,白天提交的任务会在队列里“攒团”,等位置够了再一次性启动;而高优先级的夜间任务可以随时插队。

第三个是自动化调参和训练流水线。Katib会给一堆超参数组合生成多个训练作业,这些作业如果不加管理,很可能一涌而上抢资源。配合PriorityClass,可以把搜索阶段的任务优先级调低,把最终选定的模型训练优先级调高,保证出结果的速度。

七、技术优缺点和注意事项

7.1 优点

  • 资源不再被“部分运行”的任务锁死,GPU利用率能明显提升。
  • 不同团队之间有了明确边界,吵架变少。
  • 重要任务能优先跑,业务连续性更有保障。
  • Kubeflow本身就是云原生方案,这些调度优化都是Kubernetes标准能力,学一套通用性很强。

7.2 缺点

  • 引入Volcano等多一层调度器,集群运维变复杂。不是“装上就完事”,还得监控它。
  • PriorityClass如果设置得不合理,低优先级任务可能永远跑不上,要小心设计。
  • 资源配额设得太死,反而浪费空闲资源。比如一个团队用不完,另一个团队想借却借不了。

7.3 注意事项

  • 显卡资源要提前配好nvidia-device-plugin,否则永远调度不到GPU。
  • ResourceQuota和LimitRange要匹配,不然可能相互冲突。比如LimitRange默认值设得比Quota大,会导致任何Pod都创建不了。
  • 使用Volcano时,注意它和Kubernetes新版本的兼容性,不要只图新鲜强行升级。
  • 优先级和抢占不是万能的,有时会带来Pod重启,训练任务需要支持断点续跑,不然被抢占的Pod一到,训练进度全没了。

八、总结

思路其实不复杂:第一把资源按团队划好边界,第二让作业整体调度而不是七零八落,第三给任务分三六九等。Kubeflow站在这三件套上,把分布式训练变成一个个可声明的资源,你只需要写好配置,剩下的交给平台。真正干活的还是Kubernetes的调度思想。只要吃透了这几点,再大的训练集群也能稳稳拿捏。