Support Heterogenous Node Groups - #4094
Conversation
| if not _is_node_group_master(job, replica_jobs): | ||
| return [job] |
There was a problem hiding this comment.
So group masters can now provision the jobs within its group? This seems to conflict with _release_replica_jobs_from_master_wait that still releases all the jobs when the global master is provisioned – both group masters and group non-master jobs provision concurrently.
There was a problem hiding this comment.
Proposed staged unlock / provision flow
Layout
- Group 0: jobs
0, 1, 2— job0= global master + group-0 master - Group 1: jobs
3, 4— job3= group-1 master - Group 2: jobs
5, 6— job5= group-2 master
Invariant: later groups stay blocked (waiting_master_job) until the global master (job 0) has finished its provision attempt. After that, unlock is staged (not “release everyone”).
1. Job 0 runs
jobs_to_provision = [0, 1, 2]
| Backend | Result |
|---|---|
| ComputeGroup | One run_jobs → VMs for 0, 1, 2 |
| Non-ComputeGroup | This call only creates a VM for job 0 |
2. Job 0 unlocks (after its provision attempt)
| Unlock | Why |
|---|---|
Jobs 1, 2 (rest of same group) |
Non-ComputeGroup: they still need to provision one-by-one |
Job 3 (group-1 master only) |
Lets group 1 start its batch |
Job 5 (group-2 master only) |
Lets group 2 start when ready |
Not 4 or 6 |
Avoids racing the group master’s batch |
3. Job 3 runs (group 1)
jobs_to_provision = [3, 4]
| Backend | Result |
|---|---|
| ComputeGroup | One run_jobs → VMs for 3, 4; job 4 already done |
| Non-ComputeGroup | VM for job 3 only → job 3 then unlocks job 4 → 4 provisions alone |
4. Job 5 runs (group 2)
Same pattern as step 3 with jobs_to_provision = [5, 6].
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Please update the docs on node groups in the same PR or submit another PR for an easier review. |
r4victor
left a comment
There was a problem hiding this comment.
group resources/image never get server-side defaults or validation
set_run_spec_resources_defaults, _validate_gpu_vendor_and_image, and _validate_cpu_arch_and_image in server/services/runs/spec.py all special-case service replica_groups but were not extended to task node_groups.
- top-level resources: {gpu: MI300X}, no image -> rejected ("image must be set ... ['amd']")
- group resources: {gpu: MI300X}, no image -> ACCEPTED; group gpu.vendor=None, cpu.arch=None
- group resources: {gpu: GB200} -> group cpu.arch=None (should be ARM), gpu.vendor=None
- top-level resources (unused for groups) -> gpu.vendor=nvidia, cpu.arch=x86
| if any(find_groups_ip_refs(c) for c in commands): | ||
| nodes_view = _build_nodes_ip_view(context.run.jobs, context.job.job_spec.replica_num) | ||
| try: | ||
| if not _referenced_ips_ready(commands, nodes_view): |
There was a problem hiding this comment.
What if some other nodes never get an internal ip?
There was a problem hiding this comment.
If a peer nodes never gets an IP, that peer should still fail through normal job/run logic:
-
PROVISIONING / PULLING — if the runner never becomes ready, the peer hits WAITING_RUNNER_LIMIT_EXCEEDED → peer FAILED → run torn down.
-
No capacity — peer fails with FAILED_TO_START_DUE_TO_NO_CAPACITY, retries until retry.duration then RETRY_LIMIT_EXCEEDED → run torn down.
Add heterogeneous node groups for multinode tasks (groups:), mutually exclusive with homogeneous nodes:, so one run can mix roles/resources (e.g. CPU router + GPU prefill/decode for PD-disaggregation).
Expand groups into a single global rank space with per-job node_group_* metadata; provision each group as its own same-shape batch; wire cross-group addressing via ${{ groups[i].nodes[j].IP_ADDRESS }} after IPs are ready (workers-first supported).
Runner: accept ClusterInfo.gpus_per_node, sum it for $DSTACK_GPUS_NUM, and write the MPI hostfile with per-IP slots= (CPU nodes omit slots); fall back to homogeneous gpus_per_job when the list is empty (older servers).
Design doc: Heterogeneous Node Groups
Sample config (PD-disaggregation)
SSH-Fleet