All Products
Search
Document Center

Container Service for Kubernetes:Automate RayJob execution in an ACK cluster

Last Updated:Jun 24, 2026

Set up ElasticQuotaTree and Kube Queue to automate cross-team RayJob scheduling with quotas, preemption, gang scheduling (co-scheduling), and dynamic priority adjustment so high-priority jobs get resources first.ElasticQuotaTree and Kube Queue to automate cross-team RayJob scheduling with quotas, preemption, and gang scheduling so high-priority jobs get resources first.

Prerequisites

Make sure you have:

Important

The examples use rayproject/ray:2.36.1. If pulls from Docker Hub fail due to network issues, use one of these alternatives:

How it works

Resource management follows a three-layer pipeline:

  1. Define quotas with ElasticQuotaTree — a tree where each node sets resource guarantees and caps per team or department.

  2. Automate queue creation — Kube Queue reads the ElasticQuotaTree and creates a Queue for each leaf node. RayJobs in a bound namespace route to the matching queue automatically.

  3. Control job scheduling — a RayJob with suspend: true enters its queue. Kube Queue sets suspend to false when the job's resource request fits the available quota, letting it proceed to scheduling.

For distributed tasks requiring simultaneous startup, gang scheduling prevents partial allocation and deadlocks.

Set up resource quotas

ElasticQuotaTree defines the cluster's quota hierarchy. Each node specifies:

  • min: guaranteed minimum resources for a team or department

  • max: maximum resources it can use (borrowed from siblings if available)

When a job's guaranteed minimum cannot be met, the scheduler reclaims resources from quotas exceeding their guaranteed minimum — enabling preemption for high-priority jobs.

Only one ElasticQuotaTree is supported per cluster, and it must be in the kube-system namespace.

  1. Apply the ElasticQuotaTree configuration. This example creates a three-level hierarchy: rootalgorithm department → video team, with video bound to the video namespace.

    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: video
    
    ---
    apiVersion: scheduling.sigs.k8s.io/v1beta1
    kind: ElasticQuotaTree
    metadata:
      name: elasticquotatree        # Only one ElasticQuotaTree is supported per cluster
      namespace: kube-system        # Must be created in kube-system to take effect
    spec:
      root:
        name: root
        min:
          cpu: 100
          memory: 50Gi
          nvidia.com/gpu: 16
        max:
          cpu: 100
          memory: 50Gi
          nvidia.com/gpu: 16
        children:
        - name: algorithm
          min:
            cpu: 50
            memory: 25Gi
            nvidia.com/gpu: 10
          max:
            cpu: 80
            memory: 50Gi
            nvidia.com/gpu: 14
          children:
          - name: video
            min:
              cpu: 12
              memory: 12Gi
              nvidia.com/gpu: 2
            max:
              cpu: 14
              memory: 14Gi
              nvidia.com/gpu: 4
            namespaces:
            - video                 # Jobs in this namespace count against the video quota

    Jobs marked intern-text or intern-video have a guaranteed minimum of 0, so when an algorithm team member submits an urgent job while intern tasks are running, the scheduler can preempt the intern tasks' resources for the higher-priority job.

  2. Verify the ElasticQuotaTree is active in kube-system.

    kubectl -n kube-system get elasticquotatree elasticquotatree -o yaml

View job queues

After the ElasticQuotaTree is applied, Kube Queue creates a Queue for each leaf node. For the video team under algorithm, the queue root-algorithm-video is created in the kube-queue namespace.

When a RayJob with suspend: true is submitted in the video namespace, Kube Queue:

  1. Creates a QueueUnit resource

  2. Places the job in the root-algorithm-video queue

  3. Calculates total resource requirements: Head Pod requests + (replicas × single Pod request per WorkerGroup)

  4. Sets suspend to false when quota is available, letting the job proceed to scheduling

image
  1. Check the auto-created queue for the video team.

    kubectl get queue -n kube-queue root-algorithm-video-k42kq -o yaml

    The output is similar to:

    apiVersion: scheduling.x-k8s.io/v1alpha1
    kind: Queue
    metadata:
      annotations:
        kube-queue/parent-quota-fullname: algorithm
        kube-queue/quota-fullname: root/algorithm/video
      generateName: root-algorithm-video-
      name: root-algorithm-video-k42kq
      namespace: kube-queue
    spec:
      queuePolicy: Round
    status:
      queueItemDetails:
        active: []    # Jobs awaiting scheduling, ordered by priority
        backoff: []   # Jobs waiting to retry after a failed scheduling attempt
  2. List all queues to confirm the full hierarchy.

    kubectl get queue -n kube-queue

    The output is similar to:

    NAME                               AGE
    root-algorithm-n54fm               51s
    root-algorithm-text-hgbvz          51s
    root-algorithm-video-k42kq         51s
    root-devops-2zccw                  51s
    root-infrastructure-devops-d6zqq   51s
    root-infrastructure-vbpkt          51s
    root-k8htb                         51s

Create and submit RayJobs

Create the ConfigMap

Create a ConfigMap in the video namespace with the Python code for the RayJob.

apiVersion: v1
kind: ConfigMap
metadata:
  name: rayjob-video
  namespace: video
data:
  sample_code.py: |
    import ray
    import os
    import requests

    ray.init()

    @ray.remote
    class Counter:
        def __init__(self):
            # Used to verify runtimeEnv
            self.name = os.getenv("counter_name")
            assert self.name == "test_counter"
            self.counter = 0

        def inc(self):
            self.counter += 1

        def get_counter(self):
            return "{} got {}".format(self.name, self.counter)

    counter = Counter.remote()

    for _ in range(2):
        ray.get(counter.inc.remote())
        print(ray.get(counter.get_counter.remote()))

    # Verify that the correct runtime env was used for the job.
    assert requests.__version__ == "2.26.0"

Submit RayJobs

Submit jobs in the video namespace with this manifest. Jobs here are automatically associated with the root-algorithm-video queue and video resource quota.

apiVersion: ray.io/v1
kind: RayJob
metadata:
  labels:
    job-type: video
  generateName: rayjob-video-
  namespace: video
spec:
  entrypoint: python /home/ray/samples/sample_code.py
  runtimeEnvYAML: |
    pip:
      - requests==2.26.0
      - pendulum==2.1.2
    env_vars:
      counter_name: "test_counter"

  # Delete the RayJob 10 seconds after it finishes
  ttlSecondsAfterFinished: 10

  # Required: shut down the RayCluster after the job finishes.
  # Prevents resource leaks. Must be true for queued jobs.
  shutdownAfterJobFinishes: true

  # Required: only RayJobs with suspend: true enter the Kube Queue.
  # Kube Queue sets this to false automatically when quota is available.
  suspend: true

  submissionMode: K8sJobMode

  rayClusterSpec:
    rayVersion: '2.36.1'
    headGroupSpec:
      rayStartParams:
        dashboard-host: '0.0.0.0'
        num-cpus: "0"
      template:
        spec:
          containers:
            - name: ray-head
              image: rayproject/ray:2.36.1
              ports:
                - containerPort: 6379
                  name: gcs-server
                - containerPort: 8265
                  name: dashboard
                - containerPort: 10001
                  name: client
              resources:
                limits:
                  cpu: "4"
                  memory: 4G
                requests:
                  cpu: "4"
                  memory: 4G
              volumeMounts:
                - mountPath: /home/ray/samples
                  name: code-sample
          volumes:
            - name: code-sample
              configMap:
                name: rayjob-video
                items:
                  - key: sample_code.py
                    path: sample_code.py
    workerGroupSpecs:
      - replicas: 2
        groupName: small-group
        rayStartParams: {}
        template:
          spec:
            containers:
              - name: ray-worker
                image: rayproject/ray:2.36.1
                lifecycle:
                  preStop:
                    exec:
                      command: ["/bin/sh", "-c", "ray stop"]
                resources:
                  limits:
                    cpu: "4"
                    memory: 4G
                  requests:
                    cpu: "4"
                    memory: 4G

Key queuing parameters:

Parameter Description
namespace: video Associates the job with the root-algorithm-video queue and video resource quota.
submissionMode: K8sJobMode Submits the Ray task to the RayCluster through a Kubernetes Job resource. Only RayJobs with suspend: true enter the Kube Queue.suspend: true are managed by Kube Queue.
suspend: true Places the RayJob in the queue. Kube Queue sets this to false automatically when quota is available.
shutdownAfterJobFinishes: true Required for queued jobs. Deletes the RayCluster after the job finishes to prevent resource leaks.
ttlSecondsAfterFinished: 10 Seconds to wait after the job completes before deleting the RayJob.

Verify queuing behavior

  1. Create two RayJobs with kubectl create -f.

    kubectl get rayjob -n video

    Expected output:

    NAME                 JOB STATUS   DEPLOYMENT STATUS   START TIME             END TIME   AGE
    rayjob-video-g2lvn                Initializing        2025-01-10T01:36:24Z              6s
    rayjob-video-h4x2q                Suspended           2025-01-10T01:36:25Z              5s

    rayjob-video-g2lvn dequeued and is initializing. rayjob-video-h4x2q remains suspended, waiting for quota.

  2. Confirm enqueue and dequeue timestamps for the first job.

    kubectl -n video get rayjob rayjob-video-g2lvn -o yaml

    Both timestamps confirm the job was dequeued:

    annotations:
      kube-queue/job-dequeue-timestamp: 2025-01-10 01:36:24.641181026 +0000 UTC
      kube-queue/job-enqueue-timestamp: 2025-01-10 01:36:24.298639916 +0000 UTC

    The second job shows only an enqueue timestamp — not yet scheduled:

    kubectl -n video get rayjob rayjob-video-h4x2q -o yaml
    annotations:
      kube-queue/job-enqueue-timestamp: 2025-01-10 01:36:25.505182364 +0000 UTC
  3. Check which Pods are running.

    kubectl -n video get pod

    Expected output — only Pods for the first job are running:

    NAME                                                           READY   STATUS    RESTARTS   AGE
    rayjob-video-g2lvn-9gz66                                       1/1     Running   0          28s
    rayjob-video-g2lvn-raycluster-v8tfh-head-6trq5                 1/1     Running   0          49s
    rayjob-video-g2lvn-raycluster-v8tfh-small-group-worker-hkt7m   1/1     Running   0          49s
    rayjob-video-g2lvn-raycluster-v8tfh-small-group-worker-rbzjn   1/1     Running   0          49s
  4. Check the queue to see the second job waiting.

    kubectl -n kube-queue get queue root-algorithm-video-k42kq -o yaml

    The second job appears in backoff, waiting for resources:

    status:
      queueItemDetails:
        active: []
        backoff:
        - name: rayjob-video-h4x2q-ray-qu
          namespace: video
          position: 1

Enable gang scheduling

Gang scheduling ensures all Pods in a distributed job schedule simultaneously. Without it, partial scheduling can leave a job waiting indefinitely.

Use gang scheduling when all nodes must start simultaneously:

  • Large-scale ML training: multi-node training jobs where all workers must start together

  • MPI jobs: master and worker processes that communicate from startup

  • Streaming and analytics: jobs that process data in parallel across many nodes

  • Custom distributed apps: game matchmaking services, IoT data collection pipelines

Add the ray.io/scheduler-name: kube-scheduler label to the RayJob metadata. The KubeRay Operator then injects gang scheduling labels onto every Pod it creates.

apiVersion: ray.io/v1
kind: RayJob
metadata:
  generateName: rayjob-sample-
  namespace: algorithm-text
  labels:
    ray.io/scheduler-name: kube-scheduler          # Enable gang scheduling
    quota.scheduling.alibabacloud.com/name: algorithm-video  # Bind to this quota
spec:
  entrypoint: python /home/ray/samples/sample_code.py
  runtimeEnvYAML: |
    pip:
      - requests==2.26.0
      - pendulum==2.1.2
    env_vars:
      counter_name: "test_counter"
  shutdownAfterJobFinishes: true
  suspend: true

  rayClusterSpec:
    rayVersion: '2.9.0'
    headGroupSpec:
      rayStartParams:
        dashboard-host: '0.0.0.0'
      template:
        spec:
          containers:
            - name: ray-head
              image: rayproject/ray:2.9.0
              ports:
                - containerPort: 6379
                  name: gcs-server
                - containerPort: 8265
                  name: dashboard
                - containerPort: 10001
                  name: client
              resources:
                limits:
                  cpu: "1"
                requests:
                  cpu: "1"
              volumeMounts:
                - mountPath: /home/ray/samples
                  name: code-sample
          volumes:
            - name: code-sample
              configMap:
                name: ray-job-code-sample
                items:
                  - key: sample_code.py
                    path: sample_code.py
    workerGroupSpecs:
      - replicas: 30
        groupName: small-group
        rayStartParams: {}
        template:
          spec:
            containers:
              - name: ray-worker
                image: rayproject/ray:2.9.0
                lifecycle:
                  preStop:
                    exec:
                      command: ["/bin/sh", "-c", "ray stop"]
                resources:
                  limits:
                    cpu: "1"
                  requests:
                    cpu: "1"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ray-job-code-sample
  namespace: algorithm-text
data:
  sample_code.py: |
    import ray
    import os
    import requests

    ray.init()

    @ray.remote
    class Counter:
        def __init__(self):
            self.name = os.getenv("counter_name")
            assert self.name == "test_counter"
            self.counter = 0

        def inc(self):
            self.counter += 1

        def get_counter(self):
            return "{} got {}".format(self.name, self.counter)

    counter = Counter.remote()

    for _ in range(5):
        ray.get(counter.inc.remote())
        print(ray.get(counter.get_counter.remote()))

    assert requests.__version__ == "2.26.0"

    import time
    time.sleep(30)

The KubeRay Operator injects these labels on each Pod to enable gang scheduling:

Label Value Purpose
pod-group.scheduling.sigs.k8s.io/min-available 31 (1 head + 30 workers) Minimum Pods that must schedule together
pod-group.scheduling.sigs.k8s.io/name RayCluster name Groups Pods into the same scheduling unit
quota.scheduling.alibabacloud.com/name algorithm-video Binds Pods to the specified quota
scheduling.x-k8s.io/pod-group RayCluster name Pod group reference for the scheduler

Troubleshoot gang scheduling failures

If resources are insufficient, the scheduler logs a GangFailedScheduling warning event for each Pod that could not be placed. Filter these events to identify the cause:

kubectl get events -n algorithm-text \
  --field-selector='type=Warning,reason=GangFailedScheduling' \
  | grep "cycle 1"

Expected output:

5m48s  Warning  GangFailedScheduling  pod/rayjob-sample-dtmtl-raycluster-r9jc7-small-group-worker-89mlq  rayjob-sample-dtmtl-raycluster-r9jc7-small-group-worker-89mlq in gang failed to be scheduled in cycle 1: 0/0 nodes are available: 3 Insufficient cpu.
5m48s  Warning  GangFailedScheduling  pod/rayjob-sample-dtmtl-raycluster-r9jc7-small-group-worker-8fwmr  rayjob-sample-dtmtl-raycluster-r9jc7-small-group-worker-8fwmr in gang failed to be scheduled in cycle 1: 0/0 nodes are available: 3 Insufficient cpu.

Each event includes a cycle xx number identifying the scheduling attempt and the failure reason. Use this to determine if the issue is insufficient CPU, memory, GPU, or untolerated taints.

Next steps