Heterogeneous Volcano tasks¶
Status¶
State: Implemented.
Context¶
Some workloads need a CPU coordinator and multiple GPU worker types inside one gang-scheduled Job. Independent submissions lose one lifecycle, shared service discovery, and all-or-nothing scheduling.
Goals¶
- Render one Volcano Job with multiple heterogeneous Tasks.
- Configure topology entirely in YAML.
- Allow per-Task image, setup, command, env, resources, and node selector.
- Share Job name, queue, code upload, storage, and deletion.
- Preserve the current homogeneous path.
Non-goals¶
- Multiple independent
jobs[]submissions or workflow dependencies. - Automatic balancing of mixed GPU generations.
- Arbitrary Pod spec passthrough.
- Per-Task code archives or storage in the first version.
- AML or Singularity heterogeneous nodes.
Design¶
Schema¶
Tasks are keyed mappings under the Volcano-owned _extra namespace:
base:
config:
target:
service: volcano
namespace: training
queue: default
context: <KUBECTL_CONTEXT>
environment:
image: common-runtime:latest
setup: [python -m pip install -e .]
jobs:
- name: train
sku: heterogeneous
submit_args:
env: {LOG_LEVEL: INFO}
container_args: {shm_size: 64Gi}
_extra:
volcano:
tasks:
master:
replicas: 1
cpus_per_node: 16
memory: 64Gi
gpus_per_node: 0
rdma: false
processes_per_node: 1
node_selector: {node-type: cpu}
environment:
image: coordinator-runtime:latest
setup: [python -m pip install coordinator]
command: [python coordinator.py]
env: {SERVICE_PORT: "9000"}
a100-worker:
replicas: 2
cpus_per_node: 96
memory: 512Gi
gpus_per_node: 8
rdma: true
processes_per_node: 8
node_selector:
nvidia.com/gpu.product: A100-SXM4-80GB
# command omitted: inherit the template and CLI command
h100-worker:
replicas: 4
cpus_per_node: 96
memory: 1Ti
gpus_per_node: 8
rdma: true
processes_per_node: 8
node_selector:
nvidia.com/gpu.product: H100-80GB-HBM3
environment:
image: h100-runtime:latest
setup:
- python -m pip install -e .
- python -m pip install transformer-engine
command: [python h100_train.py]
A mapping fits template inheritance: child templates override fields by Task name without list-index coupling. V1 does not add reusable node profiles.
With:
a100-worker runs python train.py; master and h100-worker use their
explicit commands.
Fields and inheritance¶
| Field | Rule |
|---|---|
| replicas and resources | required per Task |
node_selector |
empty by default |
| image | inherits top-level environment |
| setup | inherits; explicit Task list replaces |
| command | inherits the template plus CLI command; explicit Task replaces |
| env and container args | merge; Task wins |
Job-level namespace, queue, context, priority, labels, code, and storage remain shared. Global target CPU, memory, GPU, and RDMA fields are rejected in this mode to avoid ambiguous inheritance.
Typed data remains backend-local:
Template._extra.volcano.tasks
→ VolcanoOpts.tasks: dict[str, VolcanoTaskOpts]
→ JobSpec.backend_spec
→ one PodSpec per Volcano Task
No Volcano field is added to JobSpec.
A backend-neutral run-shape hook derives totals and rejects CLI conflicts
before SKU resolution. It sets JobSpec.nodes to total replicas and uses
neutral scalar placeholders for the non-uniform per-node fields. The Volcano
renderer reads actual values only from typed Tasks.
CLI shape¶
YAML is the only topology source. When Tasks exist:
- explicit
-n,-p, and--ppnfail; jobs[0].skumust not use{nodes}or{processes};--amltis rejected;- total nodes, GPU nodes, GPUs, and processes derive from Tasks.
The CLI command remains required in V1 and acts as the default Task command. Homogeneous templates retain current CLI behavior.
Manifest and rank¶
Each mapping entry becomes one spec.tasks[] entry. minAvailable is the sum
of replicas. Ordering is deterministic: master, then remaining names sorted
lexicographically.
master has rank base 0. Every later base is the sum of preceding replicas:
Each Pod receives:
| Variable | Meaning |
|---|---|
AJ_TASK_NAME |
Task name |
AJ_TASK_INDEX |
replica index within the Task |
AJ_TASK_REPLICAS |
replicas in this Task |
AJ_NODE_RANK |
unique node rank across Tasks |
AJ_NODES |
total replicas |
AJ_GPU_NODES |
replicas with GPUs |
AJ_TOTAL_GPUS |
sum of replicas × GPUs per node |
AJ_GPUS_PER_NODE |
current Task value |
AJ_PROCESSES_PER_NODE |
current Task value |
AJ_PROCESSES remains the historical alias for total GPUs. WORLD_SIZE
remains node-level, NODE_RANK defaults to AJ_NODE_RANK, and
MASTER_ADDR remains <job>-master-0.<job>.
aj does not create a process-level mixed-hardware DDP group. User code must choose participating roles and remap ranks where needed.
Invariants¶
- Exactly one
masterTask with one replica. - Task names are valid DNS-1035 labels.
- Replicas and process counts are positive.
- CPU counts are positive, GPU counts are non-negative, and memory is a valid quantity.
- Mount paths do not overlap after inheritance.
- Raw security context, hostPath, arbitrary volume, and privileged fields are rejected.
shared/job/build.pyremains service-agnostic.- Legacy manifests do not change when Tasks are absent.
Failure handling¶
All schema, resource, CLI-conflict, selector, quantity, and mount validation runs before upload or cluster mutation. Dry-run shows the expanded Task shape. An unavailable node selector remains a normal schedulability failure and is diagnosed through Job, Pod, and event views.
Evolution¶
Deferred extensions include named profiles, Task storage, Task archives, optional CLI command, custom affinity/tolerations, and other backends.