使用 RayJob 和 Kueue 进行组调度#

本指南演示了如何使用 Kueue 对 RayJob 资源进行组调度,利用 Kubernetes 上的动态资源供应和排队机制。为了说明这些概念,本指南使用了使用 Ray Data 微调 PyTorch Lightning 文本分类器示例。

组调度#

Kubernetes 中的组调度确保一组相关的 Pod(例如 Ray 集群中的 Pod)仅在所有所需资源都可用时才启动。在使用昂贵、有限的资源(例如 GPU)时,此要求至关重要。

Kueue#

Kueue 是一个 Kubernetes 原生系统,用于管理配额以及作业如何消耗它们。Kueue 决定何时

  • 让作业等待。

  • 允许作业启动,这会触发 Kubernetes 创建 Pod。

  • 抢占作业,这会触发 Kubernetes 删除活动 Pod。

Kueue 对某些 KubeRay API 提供原生支持。具体来说,你可以使用 Kueue 管理 RayJob 和 RayCluster 消耗的资源。请参阅 Kueue 文档以了解更多信息。

为何使用组调度#

在使用昂贵、有限的硬件加速器(例如 GPU)时,组调度至关重要。它防止 RayJob 部分供应 Ray 集群并占用 GPU 但不使用。Kueue 会暂停 RayJob,直到 Kubernetes 集群和底层云提供商可以保证 RayJob 执行所需的容量。这种方法极大地提高了 GPU 利用率并降低了成本,尤其是在 GPU 可用性有限的情况下。

在 GKE 上创建 Kubernetes 集群#

使用 enable-autoscaling 选项创建 GKE 集群

gcloud container clusters create kuberay-gpu-cluster \
    --num-nodes=1 --min-nodes 0 --max-nodes 1 --enable-autoscaling \
    --zone=us-east4-c --machine-type e2-standard-4

创建启用 enable-queued-provisioning 选项的 GPU 节点池

gcloud container node-pools create gpu-node-pool \
  --accelerator type=nvidia-l4,count=1,gpu-driver-version=latest \
  --enable-queued-provisioning \
  --reservation-affinity=none  \
  --zone us-east4-c \
  --cluster kuberay-gpu-cluster \
  --num-nodes 0 \
  --min-nodes 0 \
  --max-nodes 10 \
  --enable-autoscaling \
  --machine-type g2-standard-4

此命令创建一个节点池,该节点池最初有零个节点。 --enable-queued-provisioning 标志使用 ProvisioningRequest API 在 Kubernetes 节点自动伸缩器中启用“排队供应”。更多详细信息如下。你需要使用 --reservation-affinity=none 标志,因为 GKE 不支持使用 ProvisioningRequest 进行节点预留。

安装 KubeRay operator#

按照部署 KubeRay operator 的说明,从 Helm 仓库安装最新的稳定版 KubeRay operator。如果你为 GPU 节点池正确设置了污点,KubeRay operator Pod 必须位于 CPU 节点上。

安装 Kueue#

安装最新发布的 Kueue 版本。

kubectl apply --server-side -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.8.2/manifests.yaml

有关安装 Kueue 的更多详细信息,请参阅Kueue 安装

配置 Kueue 以进行组调度#

接下来,配置 Kueue 进行组调度。Kueue 利用 ProvisioningRequest API 完成两个关键任务

  1. 当作业需要更多资源时,动态向集群添加新节点。

  2. 阻止等待充足资源可用的新作业的准入。

有关更多详细信息,请参阅ProvisioningRequest 的工作原理

创建 Kueue 资源#

此清单创建以下资源

  • ClusterQueue: 定义配额和公平共享规则

  • LocalQueue: 一个属于租户的命名空间队列,引用 ClusterQueue

  • ResourceFlavor: 定义集群中可用的资源,通常来自节点

  • AdmissionCheck: 一种允许组件影响工作负载准入时机的机制

# kueue-resources.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: "default-flavor"
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: AdmissionCheck
metadata:
  name: rayjob-gpu
spec:
  controllerName: kueue.x-k8s.io/provisioning-request
  parameters:
    apiGroup: kueue.x-k8s.io
    kind: ProvisioningRequestConfig
    name: rayjob-gpu-config
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ProvisioningRequestConfig
metadata:
  name: rayjob-gpu-config
spec:
  provisioningClassName: queued-provisioning.gke.io
  managedResources:
  - nvidia.com/gpu
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: "cluster-queue"
spec:
  namespaceSelector: {} # match all
  resourceGroups:
  - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
    flavors:
    - name: "default-flavor"
      resources:
      - name: "cpu"
        nominalQuota: 10000 # infinite quotas
      - name: "memory"
        nominalQuota: 10000Gi # infinite quotas
      - name: "nvidia.com/gpu"
        nominalQuota: 10000 # infinite quotas
  admissionChecks:
  - rayjob-gpu
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: "default"
  name: "user-queue"
spec:
  clusterQueue: "cluster-queue"

创建 Kueue 资源

kubectl apply -f kueue-resources.yaml

注意

此示例配置 Kueue 来协调 GPU 的组调度。但是,你也可以使用其他资源,例如 CPU 和内存。

部署 RayJob#

下载执行微调 PyTorch Lightning 文本分类器中所有步骤的 RayJob。其源代码也在 KubeRay 仓库中。

curl -LO https://raw.githubusercontent.com/ray-project/kuberay/master/ray-operator/config/samples/pytorch-text-classifier/ray-job.pytorch-distributed-training.yaml

在创建 RayJob 之前,修改 RayJob 元数据并添加标签,将 RayJob 分配到你之前创建的 LocalQueue

metadata:
  generateName: pytorch-text-classifier-
  labels:
    kueue.x-k8s.io/queue-name: user-queue

部署 RayJob

$ kubectl create -f ray-job.pytorch-distributed-training.yaml
rayjob.ray.io/dev-pytorch-text-classifier-r6d4p created

使用 RayJob 进行组调度#

将需要 GPU 的 RayJob 部署到最初缺少 GPU 的集群时,预期行为如下

  • 由于集群中 GPU 资源不足,Kueue 暂停 RayJob。

  • Kueue 创建 ProvisioningRequest,指定 RayJob 的 GPU 需求。

  • Kubernetes 节点自动伸缩器监控 ProvisioningRequest 并根据需要添加带有 GPU 的节点。

  • 一旦所需的 GPU 节点可用,ProvisioningRequest 就得到满足。

  • Kueue 准入 RayJob,允许 Kubernetes 将 Ray 节点调度到新供应的资源上,RayJob 执行开始。

如果 GPU 不可用,Kueue 会继续暂停 RayJob。此外,节点自动伸缩器会避免供应新节点,直到它能够完全满足 RayJob 的 GPU 需求。

创建 RayJob 后,请注意,尽管 ClusterQueue 有可用的 GPU 配额,但 RayJob 状态会立即变为 suspended

$ kubectl get rayjob pytorch-text-classifier-rj4sg -o yaml
apiVersion: ray.io/v1
kind: RayJob
metadata:
  name: pytorch-text-classifier-rj4sg
  labels:
    kueue.x-k8s.io/queue-name: user-queue
...
...
...
status:
  jobDeploymentStatus: Suspended  # RayJob suspended
  jobId: pytorch-text-classifier-rj4sg-pj9hx
  jobStatus: PENDING

Kueue 会继续暂停此 RayJob,直到其对应的 ProvisioningRequest 得到满足。使用此命令列出 ProvisioningRequest 资源及其状态

$ kubectl get provisioningrequest
NAME                                                      ACCEPTED   PROVISIONED   FAILED   AGE
rayjob-pytorch-text-classifier-nv77q-e95ec-rayjob-gpu-1   True       False         False    22s

注意输出中的两个列:ACCEPTEDPROVISIONEDACCEPTED=True 意味着 Kueue 和 Kubernetes 节点自动伸缩器已确认请求。PROVISIONED=True 意味着 Kubernetes 节点自动伸缩器已完成节点供应。一旦这两个条件都为真,ProvisioningRequest 就得到满足。

$ kubectl get provisioningrequest
NAME                                                      ACCEPTED   PROVISIONED   FAILED   AGE
rayjob-pytorch-text-classifier-nv77q-e95ec-rayjob-gpu-1   True       True          False    57s

由于示例 RayJob 需要 1 个 GPU 进行微调,因此 ProvisioningRequest 通过在 gpu-node-pool 节点池中添加单个 GPU 节点得到满足。

$ kubectl get nodes
NAME                                                  STATUS   ROLES    AGE   VERSION
gke-kuberay-gpu-cluster-default-pool-8d883840-fd6d    Ready    <none>   14m   v1.29.0-gke.1381000
gke-kuberay-gpu-cluster-gpu-node-pool-b176212e-g3db   Ready    <none>   46s   v1.29.0-gke.1381000  # new node with GPUs

一旦 ProvisioningRequest 得到满足,Kueue 会准入 RayJob。然后 Kubernetes 调度器会立即将 head 和 worker 节点放置到新供应的资源上。ProvisioningRequest 确保 Ray 集群的无缝启动,没有任何 Pod 的调度延迟。

$ kubectl get pods
NAME                                                      READY   STATUS    RESTARTS        AGE
pytorch-text-classifier-nv77q-g6z57                       1/1     Running   0               13s
torch-text-classifier-nv77q-raycluster-gstrk-head-phnfl   1/1     Running   0               6m43s