Honor --include/--exclude in the SLURM launcher - #8304
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78dbdc1416
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| srun_cmd.append('--nodelist') | ||
| srun_cmd.append(active_hosts) |
There was a problem hiding this comment.
Require every filtered Slurm host
When a filter leaves multiple active hosts but reduces the task count, such as the new --exclude worker-1:0 case, passing only --nodelist does not force Slurm to launch on all of those hosts; SchedMD documents that a lower node or processor count may use only a subset of the supplied nodelist (https://slurm.schedmd.com/srun.html). In clusters where one listed node can satisfy -n, this can silently run all ranks on a subset of active_resources instead of honoring the host set the user requested, so the Slurm translation needs to also constrain the node count/task placement for the filtered hosts.
Useful? React with 👍 / 👎.
78dbdc1 to
8fa2478
Compare
|
Good catch, and it is right. Fixed in It cannot collide with the The three parametrized cases now assert the node count alongside the host list and the task count: 3 failed / 7 passed against the unpatched source, 10 passed after. The unfiltered path is byte-for-byte unchanged and still emits no |
| total_process_count = sum(self.resource_pool.values()) | ||
| # --include/--exclude are already resolved into active_resources, so counting the | ||
| # whole pool here would ask srun for slots the user filtered out. | ||
| total_process_count = sum(len(slots) for slots in active_resources.values()) |
There was a problem hiding this comment.
I ran this against 8fa2478 in a clean container. The --include path does what the description says. One thing the description does not name: this line is unconditional, so it also changes -n for --num_gpus and --num_nodes.
runner.py trims active_resources for those two flags as well (--num_nodes at runner.py:521, --num_gpus at runner.py:529), and get_cmd now sizes the job from active_resources instead of self.resource_pool. Building the srun command at this SHA and at the merge base aa3914d, on the same two-node four-slot pool the launcher tests use:
base head
control (no flags) -n 8 -n 8
--include worker-1:0,2 -n 8 -n 2
--num_gpus 2 -n 8 -n 4
--num_nodes 1 -n 8 -n 4
The bottom two look correct to me rather than wrong: world_info_base64 is encoded from the same trimmed active_resources at runner.py:543, so -n 8 against a 4 rank world info was already inconsistent. My point is only that they are unpinned. test_slurm_runner_resource_filter parametrizes include and exclude, and test_slurm_runner asserts the unfiltered 8, so nothing covers the two flags that now also move.
Two more cases in that parametrize would cover it, though they need main()'s trim replicated the way parse_inclusion_exclusion already is, since it happens after it. Naming the wider scope in the description would work too.
How I got the numbers: I called parse_inclusion_exclusion and then replicated runner.py:521-533 in a probe, then called SlurmRunner.get_cmd. I did not run main(), and I have no slurm cluster here, so the --nodelist and --nodes semantics in your description are the only part I did not check.
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.
Sizing from active_resources also moves -n for --num_gpus and --num_nodes, which
runner.py trims the same dict for. Those counts were already inconsistent with
world_info_base64, which is encoded from the trimmed dict, so this lines them up;
the two flags now have test coverage. main()'s trim moves to
apply_num_nodes_and_gpus() unchanged so the tests can build the same resource
dict main() hands the backends.
Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
8fa2478 to
9a3bf84
Compare
|
Thanks, this is right and I have covered it in You are correct that the line is unconditional and that Did both things you offered rather than picking one. Pinned. New Not replicated in the test. You are right that replicating main()'s trim in a test would drift. Named in the description. The PR body and commit message now say that sizing from Whole file: 5 failed / 7 passed before, 12 passed after. Whole On the part you flagged as unchecked: I have no slurm cluster either, so the |
|
That covers it. Moving the trim into |
Problem
SlurmRunner.get_cmd()forwards DeepSpeed's own--include/--excludestrings tosrunand sizes the job from the unfiltered hostfile. With a two-node, four-slot hostfile:builds
Two things are wrong with that line:
srunhas no--includeoption, so it exits withunrecognized option '--include'and the job never starts.-n 8counts every slot in the pool. The user asked for 2.--excludeis broken the same way.srundoes accept that flag, but DeepSpeed'sNAME[:SLOT[,SLOT ...]]syntax is not a slurm hostlist (--exclude worker-1:0names no node slurm knows), and-nstill ignores the filter, so--exclude worker-1asks for 8 tasks on the 4 slots that are left.Fix
runner.pyalready resolves both flags intoactive_resourcesbefore callingget_cmd(), and it passes that dict in as the second argument. The other runners either use it (PDSHRunner) or explicitly reject the flags invalidate_args();SlurmRunnerwas the only one accepting them and then ignoring the resolved result.So take the process count and the host list from
active_resources, and givesrun--nodelist, which is the flag it actually has:--nodelistalone is only an upper bound. SchedMD documents that "a lower node or processor count may only require a subset of the supplied node list", so-n 7 --nodelist worker-0,worker-1could put all seven ranks onworker-0and silently drop a host the filter kept.--nodespins the count (a single number is used as both minimum and maximum). It cannot collide with the--nodesthe branch below emits, becauserunner.pyraisesCannot specify num_nodes/gpus with include/exclude, soargs.num_nodesis always-1on this path.Slot-level selection still cannot be expressed to
srun, but the task count and the node set are now what the user asked for, instead of an invalid command line.Wider scope, stated rather than left implicit
Sizing from
active_resourcesis unconditional, so it also moves-nfor--num_gpusand--num_nodes, whichrunner.pytrims the same dict for (L521 and L529):The bottom two are corrections rather than regressions:
world_info_base64is encoded from that same trimmed dict at L543, so-n 8against a four-rank world info was already inconsistent. Both are now covered by tests.To pin them without a hand-rolled copy of
main()'s trim in the test, those lines move verbatim intoapply_num_nodes_and_gpus()next toparse_inclusion_exclusion, andmain()calls it. Pure extraction, no behaviour change.Test
Added
test_slurm_runner_resource_filtertotests/unit/launcher/test_multinode_runner.py, covering--includewith slots,--excludewith slots, and--excludeof a whole host, asserting the host list, the node count and the task count for each.test_slurm_runneralso now asserts the unfiltered count and passesget_cmd()the slot lists thatrunner.pyreally hands it, rather than the raw hostfile counts.test_slurm_runner_num_nodes_and_gpuscovers the two flags above, asserting-n 4for each plus that the resource-filter branch stays silent, so--nodelistis absent and--nodesappears exactly once.The rest of
tests/unit/launcher/is unchanged at9 failed, 23 passedon a clean tree and9 failed, 28 passedhere; the 9 are pre-existingtest_user_args.pyfailures in this environment because thedeepspeedconsole script is not installed, and they reproduce identically on a clean tree.yapf --style .style.yapf -dandflake8 --config .flake8are clean on both changed files.