fix(studio): resolve UUID-form CUDA_VISIBLE_DEVICES to physical GPU indices by rsd-darshan · Pull Request #8917 · unslothai/unsloth · GitHub
Skip to content

fix(studio): resolve UUID-form CUDA_VISIBLE_DEVICES to physical GPU indices - #8917

Open
rsd-darshan wants to merge 10 commits into
unslothai:mainfrom
rsd-darshan:fix/gpu-uuid-visible-devices
Open

fix(studio): resolve UUID-form CUDA_VISIBLE_DEVICES to physical GPU indices#8917
rsd-darshan wants to merge 10 commits into
unslothai:mainfrom
rsd-darshan:fix/gpu-uuid-visible-devices

Conversation

@rsd-darshan

Copy link
Copy Markdown
Contributor

Summary

Fixes #8873. On a multi-GPU CUDA host, the per-model GPUs picker in Studio silently disappears when CUDA_VISIBLE_DEVICES is set using GPU UUIDs (GPU-<uuid>,GPU-<uuid>) instead of numeric indices -- a legitimate, common NVIDIA configuration used to keep device ordering stable across PCIe reordering.

Root cause, traced by the reporter down to the exact function: _get_parent_visible_gpu_spec() in studio/backend/utils/hardware/hardware.py gives up as soon as a CUDA_VISIBLE_DEVICES token fails int() parsing, returning numeric_ids=None for any UUID mask. Every downstream consumer treats numeric_ids=None as "physical IDs unresolvable" and falls back to relative torch ordinals, which the frontend never marks pinnable -- so the picker section is omitted entirely rather than disabled-with-a-reason, making the feature look like it doesn't exist.

Fix

  • Added nvidia.resolve_gpu_uuid_mask() in studio/backend/utils/hardware/nvidia.py, which maps a UUID mask to physical indices via nvidia-smi --query-gpu=index,uuid --format=csv,noheader, preserving the mask's token order (that order defines the visible-ordinal mapping downstream).
  • Wired it into the except ValueError branch of _get_parent_visible_gpu_spec(), gated to CUDA hosts only (not _is_rocm_spec) -- ROCm has no nvidia-smi.
  • Fails closed: anything nvidia-smi can't resolve (a MIG instance UUID, nvidia-smi missing/erroring) keeps the previous numeric_ids=None behaviour unchanged, so this is additive rather than a behavior change for cases that were already correctly falling back.

Testing

Ran studio/backend/tests/test_gpu_selection.py locally (Python 3.12, CPU-only torch, matching the CI dependency recipe) -- 57 passed / 23 deselected (the deselections match the suite's own documented GPU-only exclusions), plus the other test files that share _get_parent_visible_gpu_spec() (test_rocm_multi_gpu_vram_system_wide.py, test_system_poll_no_cuda_context.py, test_diffusion_device.py, test_gpu_selection_sandbox.py) -- no new failures. ruff check and the project's canonical formatter (scripts/run_ruff_format.py) are clean.

Added test coverage:

  • TestResolveGpuUuidMask -- unit tests for the new resolver directly (order preservation against nvidia-smi's own listing order, partial-mismatch fails closed, nvidia-smi failure/missing fails closed).
  • _get_parent_visible_gpu_spec-level tests for the resolved case, the still-unresolvable case, and confirming ROCm hosts never attempt nvidia-smi resolution.
  • An end-to-end get_backend_visible_gpu_info() test confirming a resolvable UUID mask now surfaces index_kind: "physical" instead of falling back to "relative".

I don't have multi-GPU hardware to verify the actual frontend picker rendering end-to-end, so it'd be good to get confirmation from the reporter (or anyone with a multi-GPU CUDA host using a UUID mask) that the picker now appears.

…ndices

_get_parent_visible_gpu_spec() gave up as soon as a CUDA_VISIBLE_DEVICES
token failed int() parsing, returning numeric_ids=None for any UUID mask
("GPU-<uuid>,..."). Every downstream consumer -- the per-model GPU picker
chief among them -- treats numeric_ids=None as "cannot resolve physical
IDs" and falls back to relative torch ordinals, which the frontend never
marks pinnable. A UUID mask is a legitimate, common NVIDIA configuration
(stable device ordering across PCIe reordering), so this silently hid the
picker on healthy multi-GPU CUDA hosts with no indication why.

Add nvidia.resolve_gpu_uuid_mask(), which maps a UUID mask to physical
indices via `nvidia-smi --query-gpu=index,uuid`, preserving mask order
since it defines the visible-ordinal mapping downstream. Wire it into the
CUDA branch of _get_parent_visible_gpu_spec() only -- ROCm hosts have no
nvidia-smi. Anything nvidia-smi can't resolve (a MIG instance UUID,
nvidia-smi missing entirely) keeps the previous unresolved behaviour
unchanged, so this is additive rather than a behavior change for the
cases that were already failing closed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54acf8d4e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Comment thread studio/backend/utils/hardware/hardware.py
Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Comment thread studio/backend/utils/hardware/hardware.py
@rsd-darshan

Copy link
Copy Markdown
Contributor Author

- Only trust nvidia-smi's UUID->index resolution when CUDA_DEVICE_ORDER
  is actually PCI_BUS_ID (the app's own default). nvidia-smi always
  numbers by PCI bus id regardless of CUDA_DEVICE_ORDER; if something
  upstream opted back into FASTEST_FIRST, index N from nvidia-smi and
  index N under FASTEST_FIRST can be different physical cards, so a
  resolved index handed back as explicit-selectable could select the
  wrong GPU once written to CUDA_VISIBLE_DEVICES for a worker.
- Resolve unambiguous UUID prefixes (e.g. "GPU-abcdef12"), which NVIDIA
  also accepts as a device identifier, not just full UUIDs. A prefix
  shared by more than one device is rejected rather than guessed at.
- Cache resolution results (including failures) per token tuple, so a
  missing or hung nvidia-smi doesn't pay its timeout on every poll --
  _get_parent_visible_gpu_spec() can be called multiple times per
  /api/system refresh.
- Update the gpu_ids field descriptions in models/inference.py and
  models/training.py, which still said explicit selection was
  unsupported for any UUID mask; a resolvable one is now supported.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 030e7b0a8d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Comment thread studio/backend/utils/hardware/nvidia.py
Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Comment thread studio/backend/utils/hardware/nvidia.py Outdated
- Derive the PCI-bus-order ordinal by sorting nvidia-smi's own
  pci.bus_id column ourselves, instead of trusting its index column to
  already match CUDA's PCI-bus enumeration. NVIDIA's own docs note
  device enumeration ordering isn't guaranteed consistent and
  recommend UUID or PCI bus ID for that; index was one bug away from
  being exactly the kind of value that guidance is warning about.
- Leave a MIG-enabled root GPU's UUID out of the resolution map. CUDA
  exposes MIG instances rather than the whole card in that case, so
  resolving the root UUID to a normal selectable index would silently
  pick a partitioned view of it.
- Resolve mixed numeric+UUID masks (e.g. "0,GPU-<uuid>"): numeric
  tokens pass through at the same trust level as the pre-existing
  numeric-only fast path, UUID tokens resolve as before. Previously
  any UUID token in the mask made int() parsing fail on the *whole*
  token list, but only pure-numeric or pure-UUID masks actually
  reached resolution.
- Require a prefix-matched token to actually look like "GPU-<hex>"
  before attempting the match, so a malformed token like "G" or "GPU"
  can't coincidentally string-prefix a real UUID on a single-GPU host.
- Cache failed resolutions for 30s instead of the process lifetime.
  The picker polls every 5s (floating-monitor.tsx); a transient
  nvidia-smi hiccup at Studio startup would otherwise hide the picker
  until restart. Successful resolutions still cache indefinitely --
  GPU topology doesn't change mid-process.
@rsd-darshan

Copy link
Copy Markdown
Contributor Author

The failing checks here are all pre-existing on `main`, confirmed by checking the most recent completed runs there:

  • Backend CI (Python 3.10/3.11): cancelled at the 30-minute timeout -- same suite as 3.12/3.13, just running slower on these runners.
  • Backend CI (Python 3.12/3.13): byte-for-byte identical failures to this `main` run from earlier today -- `context_length` AttributeErrors, a `youtube_router` ImportError, and GGUF stream-slot TimeoutErrors, none touching anything under `studio/backend/utils/hardware/`.
  • Backend CI (Repo tests (CPU)): same -- identical 6 failures on `main`, all in model-picker/Tauri-branding contract tests and a source-encoding scan, unrelated to this diff.
  • Security audit (pip scan-packages :: hf-stack): failing on every recent `main` run too.
  • Unsloth UI CI (Chat UI Tests): cancelled mid-run, not an actual test failure.

Nothing here touches `studio/backend/utils/hardware/{hardware,nvidia}.py`, `models/{inference,training}.py`, or `tests/test_gpu_selection.py` -- the only files this PR changes.

Also pushed two follow-up commits addressing Codex's review, including a second pass it ran after the first fix: deriving the PCI-bus ordinal from `pci.bus_id` directly instead of trusting nvidia-smi's index column, excluding MIG-enabled root GPUs from resolution, resolving mixed numeric+UUID masks, validating prefix-matched tokens actually look like a GPU UUID, and expiring failed-resolution cache entries after 30s instead of caching them for the process lifetime.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49bda6060a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
…etry probes

get_visible_gpu_utilization() and get_backend_visible_gpu_info() both key
their own nvidia-smi rows by its index column, on the codebase-wide
assumption (stated in hardware.py's "GPU index ordering" comment) that
nvidia-smi's index already matches PCI bus order. The prior commit derived
an independent ordinal by sorting pci.bus_id, which is safer in isolation
but could disagree with that index on a host where the assumption doesn't
hold -- silently attaching another card's memory/utilization to a
resolved UUID.

Verify the assumption instead of computing around it: sort by pci.bus_id
and confirm every row's index already equals its position in that sort.
If it does, resolve to nvidia-smi's own index -- consistent with every
other index-keyed probe in this file. If it doesn't, the invariant this
file leans on doesn't hold on this host and no ordinal from either source
can be trusted to line up with the others, so decline to resolve rather
than guess.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f45dbfd588

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py
Comment thread studio/backend/utils/hardware/nvidia.py
…n a TTL

_visible_ordinal_map() is keyed by physical ID, so a mask that resolves
to a repeated or aliased ID (e.g. "0,GPU-<uuid-of-0>") would collapse one
of the mask's distinct visible ordinals onto the other rather than giving
it its own device entry downstream. Reject any resolution containing
duplicate physical IDs instead.

Successful resolutions were being cached for the process lifetime.
Topology/MIG mode isn't provably immutable for a whole Studio session --
an admin can enable MIG on a previously plain card without a restart --
so a stale success could keep advertising a card as directly selectable
after that changes. Successes now revalidate on a 300s TTL; failures keep
their existing, much shorter 30s TTL.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2090bf91e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
MIG-enabled roots were excluded from the resolution map before prefix
ambiguity was checked, so a prefix shared by a normal GPU and a
MIG-enabled root would see only the normal GPU as a candidate and
resolve to it -- an otherwise-ambiguous prefix silently resolving anyway,
defeating the intended fail-closed behavior. Ambiguity is now checked
against every root UUID regardless of MIG state; only the final matched
UUID's own MIG state decides whether it's a valid resolution.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb71ec3780

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
Real CUDA_VISIBLE_DEVICES semantics truncate enumeration at the first
invalid member of a mixed mask (negative, or not a real device) --
everything listed after it is hidden from the CUDA runtime entirely, per
NVIDIA's docs (e.g. "0,2,-1,1" makes only 0 and 2 visible). A numeric
token here was accepted purely on int() parsing, with no check against
what nvidia-smi actually reported, so a later UUID token in the same mask
could resolve to a GPU the real CUDA semantics would have hidden.

Rather than replicate CUDA's exact truncation point (which would mean
returning a shorter list than the mask, forcing every downstream consumer
to special-case it), a negative or unrecognized numeric member now fails
the whole resolution -- falling back to relative ordinals is safer than
resolving a UUID that real parsing would never have exposed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ecf2f92d7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py Outdated
valid_indices for the mixed-mask numeric path included every queried GPU
index, MIG-enabled roots included, even though UUID resolution
deliberately excludes those same roots. A mixed mask could route around
the MIG protection just by spelling the index instead of the UUID (e.g.
"0,GPU-<other-uuid>" where GPU 0 is MIG-enabled). valid_indices now
excludes MIG-enabled indices too, so both forms are rejected the same way.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0cda0a359

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/utils/hardware/nvidia.py
Python's int() accepts spellings CUDA's own decimal parser wouldn't --
most notably PEP 515 digit-group underscores (int("0_0") == 0). A mixed
mask member like "0_0" would silently normalize to 0 and pass the
existing range/MIG checks, when real CUDA_VISIBLE_DEVICES parsing would
treat it as an invalid member and truncate enumeration there instead.
Numeric tokens are now required to match a plain decimal shape (ASCII
digits, optional leading "-") before conversion, so a malformed member
fails closed the same way an out-of-range or MIG-enabled index already
does, rather than being silently coerced into a valid-looking value.

Also fixes a copy-paste bug from the previous commit: the UUID-token
branch was appending the leftover numeric_idx from the prior loop
iteration instead of its own resolved idx.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4cf9984a7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread studio/backend/models/inference.py Outdated
…escriptions

The gpu_ids field descriptions said any nvidia-smi-resolvable root UUID
mask was supported, which was accurate after the first review round but
went stale once later rounds added the CUDA_DEVICE_ORDER=PCI_BUS_ID guard
and the nvidia-smi-index-vs-PCI-order consistency check -- a client
following the documented contract could submit gpu_ids for a UUID mask
that's actually still unsupported under FASTEST_FIRST or a mismatched
host, and get a validation error the docs didn't warn about.
@danielhanchen

Copy link
Copy Markdown
Member

Confirmed this hits _get_parent_visible_gpu_spec in studio/backend/utils/hardware/hardware.py, where a UUID mask still drops straight to numeric_ids None and the picker falls back to relative ordinals. Backend CI on Python 3.13 is red here and green on main, though it has been flaky lately, so could you take a look at that job while I get the rest reviewed?

@rsd-darshan

rsd-darshan commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] UUID-form CUDA_VISIBLE_DEVICES silently hides the per-model GPU picker on a healthy multi-GPU CUDA host

2 participants