unsloth/studio/backend/utils/hardware
Daniel Han 300292b2f0
fix(amd): read ROCm VRAM through amd-smi so the backend keeps no HIP context (#8669)
* Read ROCm VRAM through amd-smi so the backend keeps no HIP context

torch.cuda.mem_get_info creates a CUDA/HIP primary context that the process
never releases: 612 MiB measured on a B200 with CUDA 13, 692-712 MiB reported
elsewhere. get_device_properties and get_device_capability do not, and agree
with mem_get_info on total memory exactly.

LlamaCppBackend._get_gpu_memory asks nvidia-smi first, so NVIDIA hosts were
already clean. ROCm hosts, and NVIDIA hosts without nvidia-smi on PATH, fell
straight through to torch, so a backend serving GGUF models held ~700 MiB for
a number it only reads, on a card whose models run in a llama-server child
that then cannot use it. Two docstrings promised the probe was torch-free.

amd-smi answers the same question from a subprocess. The new branch sits
between nvidia-smi and torch, reuses utils/hardware/amd.py's runner, unit
parsing and circuit breaker, honours the visibility mask, and gives a
unified-memory APU the same host reserve and total 0 as the torch branch.

The torch fallback stays for hosts with no smi tool. Callers read an empty
result as no GPU (_resolve_auto picks a backend from it, _gpu_available gates
the embedder, the loader fit drops to CPU), so an unknown there would silently
move inference off the GPU; the context is the lesser cost.

Parity on GPU 5, amd-smi output synthesised from the live nvidia-smi reading
of the same card: free 179165 MiB and total 183359 MiB both branches, 0 MiB
context on the new path against 612 MiB on the torch path. NVIDIA numbers are
untouched, and the branch is inert off ROCm.

Rebased over #7670, which added for_llama_server and the ROCm arch gate to the
same function. The new branch returns before the gate ran, so it would have put
#7624 back for llama-server placement. _rocm_arch_gate_keep now holds the gate,
and both branches call it, so the two cannot drift. It costs no context of its
own: get_device_properties measured 0MiB against mem_get_info's 620MiB on GPU 5.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Defer to torch on unified-memory APUs, and read a single-GPU amd-smi dict for PR #8669

amd-smi reports only the dedicated VRAM carve-out on a unified-memory APU,
while HIP reports the far larger GTT pool the models actually run in. On a
128GiB Strix Halo with an 8GiB BIOS carve-out that is 8GiB against ~100GiB, so
the new branch would have handed the GGUF fitter the slice and refused models
that fit today. The branch now declines the whole host when any visible device
is a unified-memory APU: discrete ROCm cards keep the context saving, APU hosts
keep byte-identical pre-PR numbers, and the two branches cannot disagree because
only one ever answers.

Reconstructing the pool from total_gtt was the other option and was rejected:
HIP's mem_get_info on an APU is not documented as equal to GTT alone, and the
older vram / fb_memory_usage shapes carry no GTT fields, so the two branches
would drift, which is what _rocm_arch_gate_keep was unified to prevent.

Separately, _gpu_entries' documented bare single-GPU dict fallback was dead:
data.get("gpu", [data]) returns the numeric id for that shape and enumerate(int)
raises. An envelope key now only counts when its value is really a list, which
also closes enumerate(None) on {"gpus": null}.

* Translate amd-smi gpu ids into HIP ids, and answer for every visible device

amd-smi numbers its devices in discovery order over its own KFD view; HIP numbers
them by KFD node id (amdsmi computes hip_id as node_id - smallest_node_id). The two
agree on most hosts and not on all of them, and the branch was keying VRAM rows by
amd-smi's number, comparing it against HIP/ROCR/CUDA visibility tokens and then
pinning the child with it. On a host where they differ, the larger card's memory
lands on the smaller card's HIP id and a load that fit before this PR can OOM.

amd-smi list -e (ROCm 6.4.0+, amdsmi_get_gpu_enumeration_info) is the mapping AMD
publishes for exactly this. Read it and re-key, and decline when it is not readable:
an older CLI rejects -e, hip_id reads N/A when the library cannot reach the device's
KFD node, and identity is then an assumption rather than a fact. A single-GPU host
skips the call, since one card on the host forces the mapping, so the ROCm desktops
this branch mostly runs on keep the context saving on any amd-smi version. That -e
failure is kept out of the circuit breaker: an old CLI is not an amd-smi outage, and
three of them would disable the utilization poller too.

_resolve_visible_physical_ids returns None both for "no mask" and for a mask it
cannot parse, and the branch read the second as the first. ROCr accepts UUID tokens
(ROCR_VISIBLE_DEVICES=GPU-<uuid>), and amd-smi ignores the mask entirely, so a
deliberately hidden card was being offered for automatic ranking. Use the existing
_visibility_mask_is_unmappable and defer to torch, which enumerates the masked set.

Separately, get_gpu_vram_mib drops a row whose total reads 0, and what was left was
a non-empty list the caller takes as the whole host. On an APU beside a dGPU that
turned two devices into one, so the "fewer than 2 usable GPUs" arm switched off
tensor parallelism on a host main reports as two. get_gpu_vram_report now also
returns every id amd-smi enumerated, and the branch answers for every visible device
or declines entirely.

The existing rocm fixture only patched _resolve_visible_physical_ids and left the
shell's own CUDA_VISIBLE_DEVICES in the environment, which now reads as a mask set
but unresolvable; clear it there too, the same trap as #8662.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Decline the amd-smi VRAM branch when HIP cannot open a device

amd-smi reads the driver over sysfs and libdrm, HIP needs /dev/kfd and a
matching HSA runtime. They come apart on a container started with
--device=/dev/dri but no --device=/dev/kfd, and on a torch wheel built
against a ROCm the installed runtime does not match: amd-smi lists the card
and hipGetDeviceCount returns 0, so torch.cuda.is_available() is False while
_torch_is_rocm() stays True.

The llama-server child is a HIP process too, so it dies exactly where torch
did. Before this branch existed the torch fallback returned [] for such a
host and the load went to CPU. The amd-smi branch returns first and hands
placement a device nothing can open.

Neither existing guard catches it, since both are torch readers that fail
open: _rocm_unified_memory_gpu_ids answers set() and _rocm_arch_by_physical_id
answers {} the moment is_available() is False, so a unified-memory APU is not
even recognised as one and its dedicated carve-out is taken for the whole
pool.

The gate costs nothing this path was not already paying:
_rocm_unified_memory_gpu_ids calls is_available() and get_device_properties()
further down the same function. Neither creates a primary context (measured:
no compute-apps entry, against 612 MiB for mem_get_info), because
is_available() is hipGetDeviceCount rather than _lazy_init.

Both rocm fixtures now declare HIP reachability instead of inheriting the
host's, the same trap as #8662: unpatched they read the test machine's own
GPU and 16 of them fail on a CPU-only runner.

* Answer the amd-smi VRAM branch only for HIP's own inventory for PR #8669

GPU_DEVICE_ORDINAL is a ROCm visibility variable that
_resolve_visible_physical_ids does not read, so the branch took no mask
from a process that had one. Defer to torch while it is set, the way
hardware.py::_rocm_visibility_mask_active gates the system-wide overlay.

Compare the visible device count against hipGetDeviceCount as well: a
device-cgroup container given one /dev/dri/renderD* node and no env var
leaves amd-smi enumerating the whole host, and a row amd-smi omitted
leaves a single-GPU shape the id-mapping shortcut accepts as a one-card
host.

Compare the totals too, with the same 10% margin the System tab's
overlay uses. An APU _rocm_classify_unified_memory cannot name (a
gfx1103 Phoenix wheel with no is_integrated flag) has amd-smi reporting
the BIOS carve-out where HIP reports the GTT pool, and a partition reads
the other way round. Devices torch cannot describe fail open.

* Decline the amd-smi VRAM branch when ROCm visibility masks are stacked

ROCR_VISIBLE_DEVICES filters and renumbers agents inside the ROCr runtime, and
clr's Device::init then indexes HIP_VISIBLE_DEVICES (or its CUDA_VISIBLE_DEVICES
twin) into the agent vector ROCr left behind, so the two compose:
ROCR_VISIBLE_DEVICES=1,2 with HIP_VISIBLE_DEVICES=0 opens physical GPU 1.

_active_gpu_visibility_mask returns the single highest-precedence value, which
reads [0] there, and on a host whose cards share a model every later check on the
amd-smi branch agrees with it: HIP opens one device, the mask names one, and the
totals match. The branch then reported physical GPU 0's free VRAM for a process
that can only reach GPU 1. Defer to torch instead, which enumerates in HIP's own
space; composing the masks would need the physical to ROCr mapping ROCr just
renumbered away.

HIP and CUDA together are one layer, not two, and Windows has no ROCr layer, so
neither defers.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-14 05:55:18 -07:00
..
__init__.py Fix Apple Silicon M4+ CPU frequency reported as MHz instead of GHz (#8571) 2026-08-12 17:02:52 -03:00
amd.py fix(amd): read ROCm VRAM through amd-smi so the backend keeps no HIP context (#8669) 2026-08-14 05:55:18 -07:00
apple.py Guard Apple GPU power against negative counter-reset readings (#6235) 2026-06-12 01:56:05 -07:00
hardware.py Studio: stop the /api/system poll from pinning a CUDA/HIP primary context (#8677) 2026-08-13 06:33:40 -07:00
nvidia.py Studio: read model text as utf-8 so umlauts survive on Windows (#7467) 2026-07-28 21:27:27 -07:00
VRAM_ESTIMATION.md Replace standalone Studio wording with Unsloth (#7221) 2026-07-19 00:47:04 -07:00
vram_estimation.py Reduce and tighten code comments and docstrings repo-wide (#6095) 2026-06-08 23:09:51 -07:00