Filter --include against the real slots, not against itself - #8239
Filter --include against the real slots, not against itself#8239vineethsaivs wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5aaeb8083a
ℹ️ 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".
|
|
||
|
|
||
| def parse_inclusion_exclusion(resource_pool, inclusion, exclusion): | ||
| # Hand parse_resource_filter what the machines actually have. Seeding this |
There was a problem hiding this comment.
Add the required Signed-off-by trailer
This non-merge commit's message does not contain a Signed-off-by: trailer, so it violates the repository's DCO/commit requirement and may be rejected by CI or maintainers; please amend the commit with --signoff using the configured author identity.
Useful? React with 👍 / 👎.
|
|
||
| for hostname, slots in resource_pool.items(): | ||
| active_resources[hostname] = node_configs[hostname] if hostname in node_configs else list(range(slots)) | ||
| active_resources[hostname] = list(range(slots)) |
There was a problem hiding this comment.
Preserve auto-masked CUDA device IDs
When no hostfile is present, main() first reads CUDA_VISIBLE_DEVICES, converts it to args.include, and only then builds resource_pool; because get_accelerator() has already queried the CUDA backend while the mask is set, the later device count can still be the masked count. With a common mask such as CUDA_VISIBLE_DEVICES=4,5, this line can build localhost: [0, 1], so the generated include localhost:4,5 is rejected as out of range instead of launching on the user-selected GPUs. Please either preserve the auto-generated visible-device list for this path or validate it against the physical slot universe.
Useful? React with 👍 / 👎.
ebarkhordar
left a comment
There was a problem hiding this comment.
The self-filtering you describe is real, and a bare hostname resolving to no slots is clearly wrong. One thing to check before this lands, because the line being removed was put there on purpose.
active_resources[hostname] = node_configs[hostname] if hostname in node_configs else list(range(slots)) came in with 5cbbff4, "Fix device selection using CUDA_VISIBLE_DEVICES (#6530)", whose commit message is "Instead of contiguous numbers based on the device count, this PR uses device indices in --include". That PR closed #5818, where the reported symptom was ValueError: No slot '2' specified on host 'localhost' under CUDA_VISIBLE_DEVICES=0,2.
That is the input whose behaviour changes here. Same container, both SHAs, pip install -e .:
from deepspeed.launcher.runner import parse_inclusion_exclusion
parse_inclusion_exclusion({"localhost": 2}, "localhost:0,2", "")- master
da066407:{'localhost': [0, 2]} - this PR
5aaeb808:ValueError: No slot '2' specified on host 'localhost'
which is #5818's error string verbatim. test_parse_inclusion_exclusion_errors pins the new behaviour deliberately, so it is a decision rather than an oversight, but I did not see #6530 addressed anywhere in the PR.
Whether the launcher actually reaches that state depends on one value I could not measure: main() builds the pair at runner.py:460 and :474, so the question is whether get_accelerator().device_count() at :471 reports the CVD-reduced count or the full one, given del os.environ[visible_devices_env] runs first at :462. I have no GPU on the machine I tested on, so I only ran the function boundary above. Reading torch, torch.cuda.device_count() now re-reads the variable unless CUDA is already initialized and caches only after init, which would make :471 return the full count and keep the CVD path working. It was decorated with lru_cache in the torch of #6530's era, which is plausibly why #5818 existed at all.
So this may be fine on current torch and broken on older, and neither the existing test_parser_* cases nor the new ones cover the CUDA_VISIBLE_DEVICES path. Would a case that pins device_count() and asserts localhost:0,2 still resolves be worth adding, so #5818 cannot come back unnoticed?
|
Two review points, one wrong and one worth answering properly. The The The mask is deleted before the count is read, though. In args.include = f"localhost:{visible_devices}"
...
del os.environ[visible_devices_env] # mask gone here
...
device_count = get_accelerator().device_count() # read here
resource_pool['localhost'] = device_countThat only helps if the count is not cached from an earlier query, and torch is explicit that it is not: # NB: Do not cache the device count prior to CUDA initialization, because
# the number of devices can change due to changes to CUDA_VISIBLE_DEVICES
# setting prior to CUDA initialization.
if _initialized:
_cached_device_count = r
Being straight about the limits of that: I established it from torch's source rather than by running it, because I do not have a CUDA box. If someone can run If you would prefer not to depend on the ordering at all, the narrow alternative is to have the |
tohtana
left a comment
There was a problem hiding this comment.
Hi @vineethsaivs, thank you for submitting this fix!
Thank you @ebarkhordar for the insightful discussion.
To clarify the behavior, I ran the check on a 4x H100 node (torch 2.9.1): with CUDA_VISIBLE_DEVICES=0,2. device_count after the del returns 4, and the include resolves to {'localhost': [0, 2]}. So the ordering holds on current torch.
If a user uses an old version or a backend caches the count again, this path regresses to #5818. But the error is clear, and this PR fixes real bugs, so I'm fine taking that trade.
Just to detect the regression, would it be possible for you to add a test for the CUDA_VISIBLE_DEVICES=0,2 case before merge? @vineethsaivs
parse_inclusion_exclusion built the "what the machines have" dict from the inclusion string, so parse_resource_filter filtered the request against itself. A bare hostname resolved to no slots, and an out-of-range slot passed the very check that exists to reject it. Build it from the resource pool instead, which is where the real slot counts live. Adds a VISIBLE_DEVICES regression test covering CUDA_VISIBLE_DEVICES=0,2: one case pinning that main() hands over the physical device count so the mask resolves to slots 0 and 2, and one pinning that a masked count is now rejected by name rather than silently launching on the wrong slots. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
5aaeb80 to
83daee0
Compare
|
Thank you @tohtana, and thank you for actually running it on a 4x H100 node rather than taking the reasoning on trust. Test added in Two cases, both CPU-only, in 1. The case you measured, This passes on 2. The trade you accepted, pinned so it is visible. Same mask, but with the accelerator reporting the masked count of 2: So the regression, if it ever comes back, is now a named error rather than a launch on the wrong GPUs, and the test says which of the two behaviours is intended. Run against unpatched Whole file: On the codex bot's P2 above, for completeness: it describes this same masked-count path and asks to either preserve the auto-generated list or validate it against the physical slot universe. This PR does the second, and case 2 is now the test for it. |
Problem
parse_inclusion_exclusion()builds thehost_infodict thatparse_resource_filter()validates against, and it seeded that dict from the inclusion string:So for any host named in
--include, the filter checked the request against a copy of itself. Two things follow.A bare hostname resolves to no slots at all.
parse_node_config_list("worker-0")gives{"worker-0": []}, soactive_resources["worker-0"]became[],parse_resource_filter's whole-node branch copied that empty list back, and the post-processing dropped the host for being empty.parse_resource_filter's own docstring usesworker-0@worker-1:0,2as its example of "use all slots on worker-0 and slots [0, 2] on worker-1":The job then launches on 2 GPUs instead of 6, with no error and no warning, and the node the user asked for first is the one that disappears.
The slot check could never fire for
--include. The same input is rejected through--excludeand accepted through--include:{'worker-0': [99]}goes on toCUDA_VISIBLE_DEVICES, so a typo surfaces as a CUDA error from inside torch rather than as the launcher error that already exists for it. Hostname validation is symmetric and works; only the slot check is affected.Fix
Hand
parse_resource_filter()the slots the machines actually have and let it do the filtering it was written to do.parse_resource_filteris unchanged: it already setsfiltered_hosts[hostname] = slotsfor an explicit slot list andhost_info[hostname]for a bare hostname, both of which are now correct.Test
tests/unit/launcher/test_run.pyhas good coverage ofparse_resource_filter, but every one of those tests hands it a correcthost_infodict directly, so nothing exercised the wrapper that builds it. That is why this was invisible: the function under test was fine, and the caller was not.Two tests added,
test_parse_inclusion_exclusionandtest_parse_inclusion_exclusion_errors, covering the bare hostname, the docstring's own example, the mixed form, exclusion, and out-of-range slots through both--includeand--exclude.python -m pytest tests/unit/launcher/test_run.pygives 2 failed / 7 passed with the source change reverted and 9 passed with it, on CPU.yapf --style .style.yapfandflake8 --config .flake8are clean on both changed files, and clean on the unmodified tree as a control.