Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions projects/amdsmi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ Full documentation for amd_smi_lib is available at [https://rocm.docs.amd.com/pr
- **Fixed GPU metrics returning no data on integrated GPUs**.
- `amdsmi_get_gpu_metrics_info()` and `amdsmi_get_gpu_activity()` failed on APUs such as gfx1103, leaving PLX temperature, GPU utilization and `amd-smi metric` empty. Those devices now report the values they provide with the rest marked N/A, and any GPU without a PLX sensor now reports N/A there instead of a placeholder reading.

- **Fixed the `amdsmi` Python module from TheRock's `rocm-sdk-core` wheel failing outside `share/amd_smi`**.
- Copied into site-packages, the module could not load `libamd_smi.so.27` unless `torch` was imported first. It now loads the library from the `_rocm_sdk_core` package on `sys.path` when no usable system library is found, and reuses the copy PyTorch already loaded instead of mapping a second one.
- The copy TheRock ships in `rocm-sdk-core` loads only that wheel's library, never a system `libamd_smi.so` from another ROCm release.
- The module refuses a library newer than itself, whose structures can be larger than the module allocates, and warns about an older one. `AMDSMI_LIB_OVERRIDE` still loads any library.

- **Fixed `amd-smi` printing a Python traceback when an unknown NIC or switch is selected**.
- `amd-smi static --nic 999` and `--switch 999` failed while building the "device not found" error, so the command exited `1` with a traceback and no readable message. `--json` and `--csv` produced no parseable output.
- Both now report `Can not find a device: NIC '999'` (or `SWITCH`) and exit `196`, matching `--gpu`, `--cpu`, and `--core`.
Expand Down
3 changes: 3 additions & 0 deletions projects/amdsmi/cmake_modules/utils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ function(get_version_from_file REL_FILE_PATH ITEM)
set(OUTPUT_ITEM "0")

if(EXISTS "${FILE_PATH}")
# Reconfigure when the version changes, so the version configured into
# files such as the Python module's _version.py matches the library.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${FILE_PATH}")
file(READ ${FILE_PATH} file_contents)
string(REGEX MATCHALL "AMDSMI_LIB_VERSION_${ITEM} *[0-9]+" OUTPUT_STR "${file_contents}")
list(LENGTH OUTPUT_STR OUTPUT_STR_LENGTH)
Expand Down
6 changes: 6 additions & 0 deletions projects/amdsmi/docs/install/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ Nightly builds of the ROCm Core SDK (including AMD SMI) are published by
Set this after activating the target virtual environment; a `PYTHONPATH`
pointing at another ROCm installation would override its Python package.

This wrapper never loads a system `libamd_smi.so`. It finds
`lib/libamd_smi.so.<MAJOR>` in the `_rocm_sdk_core` package through
`sys.path`, so a copy of the `amdsmi` directory placed elsewhere in the
environment also loads it. It refuses a library newer than itself; see
[Packaging and install paths](../packaging.md).

## Optional and advanced installation

Use these optional procedures for CLI autocompletion and advanced setups, such
Expand Down
27 changes: 23 additions & 4 deletions projects/amdsmi/docs/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ the loader contract that `py-interface/amdsmi_wrapper.py` implements.
| System package (deb/rpm) | `/opt/rocm/lib/libamd_smi.so.<MAJOR>` (+ `ld.so.conf.d` entry) | Installed into the system interpreter's `site-packages`/`dist-packages` **and** `share/amd_smi` | SONAME via the dynamic linker |
| TheRock native package (`amdrocm-amdsmi`) | `/opt/rocm/core-X.Y/lib/libamd_smi.so.<MAJOR>` | `/opt/rocm/core-X.Y/share/amd_smi/amdsmi` only; importable after adding `share/amd_smi` to `PYTHONPATH` | Resolved relative to the wrapper (`../../../lib`) |
| TheRock tarball | `<root>/lib/libamd_smi.so.<MAJOR>` in the extracted tree | `<root>/share/amd_smi/amdsmi`; add `share/amd_smi` to `PYTHONPATH` for direct imports | Resolved relative to the wrapper (`../../../lib`) |
| ROCm via pip (TheRock `rocm-sdk-core`) | `<root>/lib/libamd_smi.so.<MAJOR>` | `<root>/share/amd_smi/amdsmi`; add `share/amd_smi` to `PYTHONPATH` for direct imports | Resolved relative to the wrapper (`../../../lib`) |
| ROCm via pip (TheRock `rocm-sdk-core`) | `<root>/lib/libamd_smi.so.<MAJOR>` | `<root>/share/amd_smi/amdsmi`; add `share/amd_smi` to `PYTHONPATH` for direct imports | Resolved relative to the wrapper (`../../../lib`); a copy elsewhere, such as in site-packages, uses `_rocm_sdk_core/lib` |
| ROCm via pip in a venv | Same as above, inside the venv | Same as above, inside the venv | Same as above |
| Community-built PyPI `amdsmi` releases through 7.0.2 | Not bundled; requires a matching ROCm `libamd_smi.so` | Interpreter `site-packages` | Legacy loader checks `ROCM_HOME` before `ROCM_PATH`, then the dynamic linker and `/opt/rocm/lib` |
| Bundled-library wheel (`BUILD_PYTHON_WHEEL=ON`) | Bundled `libamd_smi_python.so` next to the wrapper | Interpreter `site-packages` | The bundled `.so`; system fallback is disabled |
Expand Down Expand Up @@ -41,9 +41,28 @@ The current `py-interface/amdsmi_wrapper.py` selects the library in this order
2. A bundled `libamd_smi_python.so` next to the wrapper — the bundled-library wheel.
3. The SONAME resolved relative to the wrapper (`parents[3]/lib`) — the TheRock
`share/amd_smi` layout, where a venv has no `ld.so.conf.d` entry.
4. The bare SONAME via the dynamic linker — the system deb/rpm.

Steps 3 and 4 are skipped when `_AMDSMI_ALLOW_SYSTEM_FALLBACK` is `False`. The
4. In the copy TheRock ships in `rocm-sdk-core` (it sets
`_AMDSMI_ROCM_SDK_CORE = True`): `lib/<SONAME>` in the `_rocm_sdk_core`
package, then stop. That copy never loads a system `libamd_smi.so`, which
could come from another ROCm release.
5. The bare SONAME via the dynamic linker — the system deb/rpm.
6. `lib/<SONAME>` in the `_rocm_sdk_core` package, for a wrapper without that
flag.

Steps 4 and 6 locate `_rocm_sdk_core` through `sys.modules` or `sys.path`
without importing it, so they also work when the module is copied out of
`share/amd_smi`, for example into site-packages. Each file in steps 3, 4 and 6
is first opened with `RTLD_NOLOAD`, so a copy already loaded in the process is
reused; PyTorch's `_amdsmi_cdll_hook` would otherwise redirect the load to
`$ROCM_PATH/lib`.

Steps 3 to 6 check the library version against the module's `_version.py`. A
library with a newer minor version, or another major version, is closed and
skipped, because SONAME `27` structures have grown between minor releases and
an older module allocates too little for them. An older library loads with a
warning. `AMDSMI_LIB_OVERRIDE` and the bundled library skip this check.

Steps 3 to 6 are skipped when `_AMDSMI_ALLOW_SYSTEM_FALLBACK` is `False`. The
committed wrapper and the system package keep it `True`; the wheel build flips
it to `False`, so the bundled-library wheel does not automatically load a
system `libamd_smi.so` (which could be a different version and would risk
Expand Down
4 changes: 2 additions & 2 deletions projects/amdsmi/py-interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The following install modes expose the same `import amdsmi` entry point:
| Mode | What ships | Loader resolves to |
|------|-----------|--------------------|
| System package (`amd-smi-lib` rpm/deb) | The wrapper installed directly into the system Python's `site-packages` so plain `import amdsmi` works. The shared library lives at `/opt/rocm/lib/libamd_smi.so` and is registered with the dynamic linker via `ldconfig`. | `libamd_smi.so` resolved by the dynamic linker (SONAME). |
| TheRock native package, tarball, or ROCm pip install | The wrapper under `<root>/share/amd_smi/amdsmi`; add `<root>/share/amd_smi` to `PYTHONPATH` for direct imports. | `<root>/lib/libamd_smi.so.<MAJOR>`, resolved relative to the wrapper. |
| TheRock native package, tarball, or ROCm pip install | The wrapper under `<root>/share/amd_smi/amdsmi`; add `<root>/share/amd_smi` to `PYTHONPATH` for direct imports. | `<root>/lib/libamd_smi.so.<MAJOR>`, resolved relative to the wrapper. A `rocm-sdk-core` wrapper copied elsewhere, such as into site-packages, loads `lib/libamd_smi.so.<MAJOR>` from the `_rocm_sdk_core` package. |
| Community-built PyPI releases through 7.0.2 | Only the Python wrapper in `<site-packages>/amdsmi/`. Its version must match the ROCm release providing the library. | Matching ROCm `libamd_smi.so`; the legacy loader checks `ROCM_HOME` before `ROCM_PATH`. |
| Bundled-library wheel (`BUILD_PYTHON_WHEEL=ON`) | The wrapper plus a SONAME-renamed `libamd_smi_python.so` directly inside `<site-packages>/amdsmi/`. | `libamd_smi_python.so` next to the wrapper. |

Expand Down Expand Up @@ -56,7 +56,7 @@ wrapper you intend to use.

| Variable | Purpose |
|----------|---------|
| `AMDSMI_LIB_OVERRIDE` | Absolute path to a `libamd_smi*.so` to load **instead of** the auto-detected one. Intended for local development against an in-tree build (e.g. `AMDSMI_LIB_OVERRIDE=$PWD/build/libamd_smi.so python3 -c "import amdsmi"`) and for ABI-compatibility tests that need to point the wrapper at a curated alternate library. When set, it takes precedence over both the pip-bundled and system libraries. |
| `AMDSMI_LIB_OVERRIDE` | Absolute path to a `libamd_smi*.so` to load **instead of** the auto-detected one. Intended for local development against an in-tree build (e.g. `AMDSMI_LIB_OVERRIDE=$PWD/build/libamd_smi.so python3 -c "import amdsmi"`) and for ABI-compatibility tests that need to point the wrapper at a curated alternate library. When set, it takes precedence over both the pip-bundled and system libraries, and the library is loaded even if it is newer than the wrapper, which the wrapper otherwise refuses. |

The legacy PyPI wrappers do not implement `AMDSMI_LIB_OVERRIDE` or the
current missing-library behavior described below.
Expand Down
166 changes: 155 additions & 11 deletions projects/amdsmi/py-interface/amdsmi_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def char_pointer_cast(string, encoding='utf-8'):
# ---------------------------------------------------------------------------
# Dynamic library loading
# ---------------------------------------------------------------------------
# This wrapper supports three self-contained install paths:
# This wrapper supports four self-contained install paths:
#
# 1. pip wheel
# The wheel ships ``libamd_smi_python.so`` next to this file.
Expand All @@ -168,6 +168,10 @@ def char_pointer_cast(string, encoding='utf-8'):
# Wrapper at ``<root>/share/amd_smi/amdsmi/``, library at
# ``<root>/lib/<SONAME>`` -- resolved by fixed relative path.
#
# 4. the copy TheRock ships in its rocm-sdk-core wheel, also once copied
# into site-packages: library at ``<_rocm_sdk_core>/lib/<SONAME>``.
# TheRock marks that copy by setting ``_AMDSMI_ROCM_SDK_CORE``.
#
# A user installs ONE of these packages. We never combine paths
# from them -- no ROCM_HOME / ROCM_PATH ladders, no walking up to a
# ROCm root, no LD_LIBRARY_PATH probing. ``AMDSMI_LIB_OVERRIDE`` stays
Expand All @@ -190,6 +194,117 @@ def char_pointer_cast(string, encoding='utf-8'):
# system library (which would risk symbol conflicts inside PyTorch / JAX).
_AMDSMI_ALLOW_SYSTEM_FALLBACK = True

# TheRock flips this to True in the copy it ships in the rocm-sdk-core wheel
# (build_tools/build_python_packages.py). That copy then loads only
# rocm-sdk-core's library, never a system libamd_smi.so found by SONAME, so a
# module from one ROCm release is not paired with another release's library.
_AMDSMI_ROCM_SDK_CORE = False


def _module_version():
"""(major, minor) of the build this module came from, read from the
_version.py that is configured beside it, or None when it is absent."""
import re

try:
text = (Path(__file__).resolve().parent / "_version.py").read_text()
except OSError:
return None
m = re.search(r'__version__\s*=\s*["\'](\d+)\.(\d+)', text)
return (int(m.group(1)), int(m.group(2))) if m else None


def _check_library_version(lib, path):
"""Reject a library newer than this module: SONAME 27 has grown structs
between minor releases, and an older module's buffers are too small for
them. An older library only lacks newer functions, so it is a warning."""
expected = _module_version()
if expected is None:
return
try:
get_lib_version = lib.amdsmi_get_lib_version
except AttributeError:
return
# amdsmi_version_t starts with uint32 major, minor; the buffer is larger
# than the struct so no future layout can overflow it.
version = (ctypes.c_uint32 * 64)()
if get_lib_version(version) != 0:
return
found = (version[0], version[1])
if found[0] != expected[0] or found[1] > expected[1]:
raise OSError(
"loading %s returned AMD SMI library %d.%d, which this %d.%d Python module cannot "
"use; set AMDSMI_LIB_OVERRIDE to a library path to load it anyway"
% ((path,) + found + expected)
)
if found[1] < expected[1]:
import warnings

warnings.warn(
"loading %s returned AMD SMI library %d.%d, older than this %d.%d Python module; "
"newer functions are unavailable" % ((path,) + found + expected),
stacklevel=3,
)


def _unload(lib):
"""Drop the reference ctypes.CDLL took on a library the version check
rejected, so a later load by SONAME, or one PyTorch's hook redirects,
cannot return it."""
import _ctypes

handle = getattr(lib, "_handle", None)
if handle is not None and hasattr(_ctypes, "dlclose"):
_ctypes.dlclose(handle)


def _rocm_sdk_core_dir():
"""The _rocm_sdk_core package directory, found the way an import would
find it but without importing it, so import guards that reject third-party
packages cannot break the load."""
import sys

module = sys.modules.get("_rocm_sdk_core")
if getattr(module, "__file__", None):
return Path(module.__file__).parent
for entry in sys.path:
if not isinstance(entry, str):
continue
candidate = Path(os.path.abspath(entry)) / "_rocm_sdk_core"
try:
if (candidate / "__init__.py").is_file():
return candidate
except OSError:
continue
return None


def _open_library(path, mode, errors, pinned=True):
"""Open path, reusing it when it is already loaded. PyTorch's import hook
can redirect a plain load to another libamd_smi; with RTLD_NOLOAD the
redirect can only return a copy the process has already loaded. A pinned
path is then opened through ctypes.cdll, which the hook does not wrap, so
the file asked for is the file loaded."""
noload = getattr(os, "RTLD_NOLOAD", 0)
for attempt in ([mode | noload] if noload else []) + [mode]:
try:
if attempt == mode and pinned:
lib = ctypes.cdll.LoadLibrary(str(path))
else:
lib = ctypes.CDLL(str(path), mode=attempt)
except OSError as err:
if attempt == mode:
errors.append(str(err))
continue
try:
_check_library_version(lib, path)
except OSError as err:
errors.append(str(err))
_unload(lib)
return None
return lib
return None


def _load_library():
"""Load the AMD SMI shared library.
Expand All @@ -198,8 +313,10 @@ def _load_library():
1. ``AMDSMI_LIB_OVERRIDE`` env var (ABI-test escape hatch).
2. ``libamd_smi_python.so`` next to this file (pip wheel).
3. ``<root>/lib/<SONAME>`` relative to this file (relocatable ROCm tree).
4. SONAME via the dynamic linker (system rpm / deb); skipped when
_AMDSMI_ALLOW_SYSTEM_FALLBACK is False (pip wheel).
4. In rocm-sdk-core's copy: ``_rocm_sdk_core/lib/<SONAME>``, then stop.
5. SONAME via the dynamic linker (system rpm / deb).
6. ``_rocm_sdk_core/lib/<SONAME>``, for a copy without TheRock's flag.
3-6 skip a library newer than this module.
"""
mode = getattr(ctypes, "RTLD_LOCAL", 0)

Expand All @@ -222,16 +339,43 @@ def _load_library():
# amdsmi_interface.py resolves librocm-core.so the same way. A
# present-but-unloadable file (missing deps) must not shadow the system
# linker lookup, so fall through on OSError.
candidates = []
here = Path(__file__).resolve()
if len(here.parents) > 3:
relocatable = here.parents[3] / "lib" / _AMDSMI_LIB_SONAME
if relocatable.exists():
try:
return ctypes.CDLL(str(relocatable), mode=mode), str(relocatable)
except OSError:
pass

return ctypes.CDLL(_AMDSMI_LIB_SONAME, mode=mode), _AMDSMI_LIB_SONAME
candidates.append(here.parents[3] / "lib" / _AMDSMI_LIB_SONAME)
if _AMDSMI_ROCM_SDK_CORE:
sdk_core = _rocm_sdk_core_dir()
if sdk_core is not None:
candidates.append(sdk_core / "lib" / _AMDSMI_LIB_SONAME)

errors = []
for path in candidates:
if path.exists():
lib = _open_library(path, mode, errors)
if lib is not None:
return lib, str(path)

if _AMDSMI_ROCM_SDK_CORE:
if sdk_core is None:
errors.append("_rocm_sdk_core not found on sys.path")
raise OSError(
"no usable %s from rocm-sdk-core: %s"
% (_AMDSMI_LIB_SONAME, "; ".join(errors) or "no file at " + ", ".join(map(str, candidates)))
)
lib = _open_library(_AMDSMI_LIB_SONAME, mode, errors, pinned=False)
if lib is not None:
return lib, _AMDSMI_LIB_SONAME

# No system library: a module copied beside TheRock's rocm-sdk-core wheel
# by hand, without TheRock's flag, still finds that wheel's library.
sdk_core = _rocm_sdk_core_dir()
if sdk_core is not None:
path = sdk_core / "lib" / _AMDSMI_LIB_SONAME
if path.exists():
lib = _open_library(path, mode, errors)
if lib is not None:
return lib, str(path)
raise OSError("; ".join(errors))


class _MissingLibrary:
Expand Down
Loading
Loading