Skip to content

Commit 8fa2478

Browse files
committed
Honor --include/--exclude in the SLURM launcher
The SLURM runner passed DeepSpeed's own resource-filter strings straight to srun and sized the job from the unfiltered hostfile, so both filters were broken: deepspeed --launcher slurm --include worker-1:0,2 train.py -> srun -n 8 --include worker-1:0,2 ... train.py srun has no --include, so it exits with "unrecognized option" and the job never starts. --exclude fares no better: srun does have that flag, but DeepSpeed's NAME[:SLOT,...] syntax is not a slurm hostlist, and -n still counts every slot in the pool rather than the ones left after filtering. runner.py already resolves both flags into active_resources before calling get_cmd(), so take the process count and the node list from there and pass srun --nodelist, which is the flag it actually has. --nodelist alone is only an upper bound, since srun documents that a lower task count "may only require a subset of the supplied node list", so pass --nodes as well to pin the host count; runner.py forbids --num_nodes alongside a resource filter, so nothing else sets it. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
1 parent 9311fd5 commit 8fa2478

2 files changed

Lines changed: 38 additions & 9 deletions

File tree

deepspeed/launcher/multinode_runner.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,9 @@ def name(self):
358358
def get_cmd(self, environment, active_resources):
359359
assert not getattr(self.args, 'detect_nvlink_pairs',
360360
False), "slurm backend does not support remapping visible devices"
361-
total_process_count = sum(self.resource_pool.values())
361+
# --include/--exclude are already resolved into active_resources, so counting the
362+
# whole pool here would ask srun for slots the user filtered out.
363+
total_process_count = sum(len(slots) for slots in active_resources.values())
362364
srun_cmd = [
363365
'srun',
364366
'-n',
@@ -368,12 +370,19 @@ def get_cmd(self, environment, active_resources):
368370
if getattr(self.args, 'slurm_comment', ''):
369371
srun_cmd += ['--comment', self.args.slurm_comment]
370372

371-
if self.args.include != "":
372-
srun_cmd.append('--include')
373-
srun_cmd.append(f'{self.args.include}')
374-
if self.args.exclude != "":
375-
srun_cmd.append('--exclude')
376-
srun_cmd.append(f'{self.args.exclude}')
373+
if self.args.include != "" or self.args.exclude != "":
374+
# srun has no --include, and the NAME[:SLOT,...] syntax DeepSpeed accepts is not
375+
# a slurm hostlist, so name the hosts that survived the filter instead.
376+
active_hosts = ",".join(active_resources.keys())
377+
srun_cmd.append('--nodelist')
378+
srun_cmd.append(active_hosts)
379+
# --nodelist alone is only an upper bound: srun documents that a lower task count
380+
# "may only require a subset of the supplied node list", so it could pack every
381+
# rank onto one host and silently drop a host the filter kept. --nodes pins the
382+
# count, and runner.py forbids --num_nodes alongside a resource filter, so the
383+
# branch below cannot also set it.
384+
srun_cmd.append('--nodes')
385+
srun_cmd.append(f'{len(active_resources)}')
377386
if self.args.num_nodes > 0:
378387
srun_cmd.append('--nodes')
379388
srun_cmd.append(f'{self.args.num_nodes}')

tests/unit/launcher/test_multinode_runner.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from copy import deepcopy
77
from deepspeed.launcher import multinode_runner as mnrunner
8-
from deepspeed.launcher.runner import encode_world_info, parse_args
8+
from deepspeed.launcher.runner import encode_world_info, parse_args, parse_inclusion_exclusion
99
import os
1010
import pytest
1111

@@ -62,9 +62,29 @@ def test_mpich_runner(runner_info):
6262

6363
def test_slurm_runner(runner_info):
6464
env, resource_pool, world_info, args = runner_info
65+
active_resources = parse_inclusion_exclusion(resource_pool, args.include, args.exclude)
6566
runner = mnrunner.SlurmRunner(args, world_info, resource_pool)
66-
cmd = runner.get_cmd(env, resource_pool)
67+
cmd = runner.get_cmd(env, active_resources)
6768
assert cmd[0] == 'srun'
69+
assert cmd[cmd.index('-n') + 1] == '8'
70+
71+
72+
@pytest.mark.parametrize('resource_filter, expected_hosts, expected_node_count, expected_process_count',
73+
[(['--include', 'worker-1:0,2'], 'worker-1', '1', '2'),
74+
(['--exclude', 'worker-1:0'], 'worker-0,worker-1', '2', '7'),
75+
(['--exclude', 'worker-1'], 'worker-0', '1', '4')])
76+
def test_slurm_runner_resource_filter(runner_info, resource_filter, expected_hosts, expected_node_count,
77+
expected_process_count):
78+
env, resource_pool, world_info, _ = runner_info
79+
args = parse_args(resource_filter + ['test_launcher.py'])
80+
active_resources = parse_inclusion_exclusion(resource_pool, args.include, args.exclude)
81+
runner = mnrunner.SlurmRunner(args, world_info, resource_pool)
82+
cmd = runner.get_cmd(env, active_resources)
83+
assert '--include' not in cmd
84+
assert cmd[cmd.index('--nodelist') + 1] == expected_hosts
85+
# Without --nodes, srun may satisfy -n from a subset of --nodelist and drop a kept host.
86+
assert cmd[cmd.index('--nodes') + 1] == expected_node_count
87+
assert cmd[cmd.index('-n') + 1] == expected_process_count
6888

6989

7090
def test_mvapich_runner(runner_info):

0 commit comments

Comments
 (0)