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
3 changes: 3 additions & 0 deletions projects/amdsmi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Full documentation for amd_smi_lib is available at [https://rocm.docs.amd.com/pr

### Resolved Issues

- **Fixed `amd-smi static --vram` reporting `GDDR7` for LPDDR5 unified memory on APUs (e.g. gfx117x)**.
- `AMDSMI_VRAM_TYPE__MAX` aliases the highest real memory type (`LPDDR5`), so a genuine LPDDR5 reading was matched by the `__MAX` special case and mislabeled `GDDR7`. It is now correctly reported as `LPDDR5`.

- **Fixed `amd-smi set -L/--clk-limit <clk> max <value>` not enforcing caps that fall between clock levels**.
- For `mclk` and `fclk` ONLY, which expose a discrete DPM table, the requested `max` is now rounded down to the nearest selectable clock level, so the enforced limit never exceeds the requested value.
- `sclk` supports a continuous frequency range, so its requested `max` is honored exactly (e.g. `600` enforces a limit of 600MHz) and is not snapped.
Expand Down
5 changes: 4 additions & 1 deletion projects/amdsmi/amdsmi_cli/subcommands/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,8 +1034,11 @@ def static_gpu(

# Get vram type string
vram_type_enum = vram_info["vram_type"]
# AMDSMI_VRAM_TYPE__MAX aliases the highest real type (LPDDR5);
# the auto-generated enum-value map resolves this shared value
# to the "__MAX" label, so translate it to the real type here.
if vram_type_enum == amdsmi_interface.amdsmi_wrapper.AMDSMI_VRAM_TYPE__MAX:
vram_type = "GDDR7"
vram_type = "LPDDR5"
else:
vram_type = amdsmi_interface.amdsmi_wrapper.amdsmi_vram_type_t__enumvalues[
vram_type_enum
Expand Down
261 changes: 261 additions & 0 deletions projects/amdsmi/tests/python/unit/gpu/test_cli_vram_type_lpddr5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
#
# Copyright (C) Advanced Micro Devices. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Mock-based unit test for ``amd-smi static --vram`` memory-type labelling.

Covers the LPDDR5 memory-type label. ``AMDSMI_VRAM_TYPE__MAX`` aliases
the highest real enum value (``LPDDR5`` == 31), and the auto-generated
``amdsmi_vram_type_t__enumvalues`` map resolves that shared key to the ``__MAX``
label. ``static.py`` special-cases the ``__MAX`` value and must translate it to
``LPDDR5``; it was previously mislabelled ``GDDR7``, which surfaced on gfx117x
APUs whose unified memory reports as LPDDR5.

``static.py`` is loaded from the source tree so the test exercises the code
under development rather than a possibly-stale installed copy.
"""

import argparse
import copy
import importlib.util
import os
import sys
import types
import unittest

_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.abspath(os.path.join(_THIS_DIR, "..", "..", "..", ".."))
STATIC_PATH = os.path.join(_REPO_ROOT, "amdsmi_cli", "subcommands", "static.py")

# Mirrors amdsmi_wrapper.amdsmi_vram_type_t__enumvalues: LPDDR5 and __MAX share
# value 31, and the later duplicate key (__MAX) wins in the dict literal, so a
# plain lookup of 31 yields "__MAX" rather than "LPDDR5".
_ENUMVALUES = {
0: "AMDSMI_VRAM_TYPE_UNKNOWN",
1: "AMDSMI_VRAM_TYPE_HBM",
22: "AMDSMI_VRAM_TYPE_GDDR6",
23: "AMDSMI_VRAM_TYPE_GDDR7",
30: "AMDSMI_VRAM_TYPE_LPDDR4",
31: "AMDSMI_VRAM_TYPE__MAX",
}
_VRAM_TYPE__MAX = 31

_BASE_VRAM_INFO = {
"vram_type": _VRAM_TYPE__MAX,
"vram_vendor": "UNKNOWN",
"vram_size": 512,
"vram_bit_width": 128,
"vram_max_bandwidth": "N/A",
}


class _FakeLibraryException(Exception):
def get_error_info(self):
return str(self)


def _install_fake_modules(holder):
"""Register a stub ``amdsmi`` package plus the sibling CLI modules.

``holder["info"]`` is the vram payload returned to ``static.py`` so each
test can swap the reported ``vram_type`` without reloading the module.
"""
amdsmi_pkg = types.ModuleType("amdsmi")
interface = types.ModuleType("amdsmi.amdsmi_interface")
exception = types.ModuleType("amdsmi.amdsmi_exception")

def _get_vram_info(_handle):
return copy.deepcopy(holder["info"])

interface.amdsmi_get_gpu_vram_info = _get_vram_info

wrapper = types.ModuleType("amdsmi.amdsmi_interface.amdsmi_wrapper")
wrapper.AMDSMI_VRAM_TYPE__MAX = _VRAM_TYPE__MAX
wrapper.amdsmi_vram_type_t__enumvalues = dict(_ENUMVALUES)
interface.amdsmi_wrapper = wrapper

exception.AmdSmiLibraryException = _FakeLibraryException

amdsmi_pkg.amdsmi_interface = interface
amdsmi_pkg.amdsmi_exception = exception
sys.modules["amdsmi"] = amdsmi_pkg
sys.modules["amdsmi.amdsmi_interface"] = interface
sys.modules["amdsmi.amdsmi_exception"] = exception

# ``static.py`` imports these sibling names at load time; the vram path
# never instantiates them (the test injects a fake helpers object).
helpers_mod = types.ModuleType("amdsmi_helpers")
helpers_mod.AMDSMIHelpers = object
sys.modules["amdsmi_helpers"] = helpers_mod

exceptions_mod = types.ModuleType("amdsmi_cli_exceptions")
exceptions_mod.AmdSmiInvalidParameterException = type(
"AmdSmiInvalidParameterException", (Exception,), {}
)
sys.modules["amdsmi_cli_exceptions"] = exceptions_mod


def _load_static_module():
spec = importlib.util.spec_from_file_location("static_under_test", STATIC_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


class _FakeLogger:
"""Captures the ``values`` payload ``static_gpu`` stores per GPU."""

def __init__(self, fmt):
self._fmt = fmt
self.captured_values = None
self.store_gpu_json_output = []

def is_json_format(self):
return self._fmt == "json"

def is_csv_format(self):
return self._fmt == "csv"

def is_human_readable_format(self):
return self._fmt == "human"

def store_output(self, _gpu, key, value):
if key == "values":
self.captured_values = value

def print_output(self, *args, **kwargs):
pass

def store_multiple_device_output(self):
pass


class _FakeHelpers:
"""Minimal helpers stub for the single-GPU, baremetal-off vram path."""

def handle_gpus(self, args, _logger, _func):
return False, args.gpu

def get_gpu_id_from_device_handle(self, _handle):
return 0

def os_info(self):
return "mock-os"

def check_required_groups(self):
pass

def is_linux(self):
return True

def is_baremetal(self):
return False

def is_virtual_os(self):
return True

def is_hypervisor(self):
return False


def _build_args():
"""Namespace with vram on and every other static section off."""
return argparse.Namespace(
gpu=object(),
asic=False,
bus=False,
vbios=False,
driver=False,
ras=False,
vram=True,
cache=False,
board=False,
process_isolation=False,
clock=False,
mem_carveout=False,
partition=False,
)


class TestCliVramTypeLpddr5(unittest.TestCase):
_SAVED_MODULE_NAMES = (
"amdsmi",
"amdsmi.amdsmi_interface",
"amdsmi.amdsmi_exception",
"amdsmi_helpers",
"amdsmi_cli_exceptions",
)

@classmethod
def setUpClass(cls):
if not os.path.isfile(STATIC_PATH):
raise unittest.SkipTest(f"amd-smi CLI static.py not found at {STATIC_PATH}")
# Snapshot any real amdsmi already loaded so the stub does not leak into
# sibling suites sharing the interpreter; restored in tearDownClass.
cls._saved_modules = {name: sys.modules.get(name) for name in cls._SAVED_MODULE_NAMES}
cls.holder = {"info": copy.deepcopy(_BASE_VRAM_INFO)}
_install_fake_modules(cls.holder)
cls.static_module = _load_static_module()

@classmethod
def tearDownClass(cls):
for name, saved in cls._saved_modules.items():
if saved is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = saved

def _run_vram(self, vram_type_value, fmt="human"):
info = copy.deepcopy(_BASE_VRAM_INFO)
info["vram_type"] = vram_type_value
self.holder["info"] = info

commands = object.__new__(self.static_module.StaticCommands)
commands.logger = _FakeLogger(fmt)
commands.helpers = _FakeHelpers()
commands.group_check_printed = True

commands.static_gpu(_build_args())

if fmt == "json":
self.assertTrue(commands.logger.store_gpu_json_output)
static_dict = commands.logger.store_gpu_json_output[-1]
else:
static_dict = commands.logger.captured_values
self.assertIsNotNone(static_dict, "static_gpu stored no values payload")
self.assertIn("vram", static_dict)
return static_dict["vram"]["type"]

def test_max_value_maps_to_lpddr5_human(self):
# AMDSMI_VRAM_TYPE__MAX == LPDDR5 == 31; must not be mislabelled GDDR7.
self.assertEqual(self._run_vram(_VRAM_TYPE__MAX, "human"), "LPDDR5")

def test_max_value_maps_to_lpddr5_json(self):
self.assertEqual(self._run_vram(_VRAM_TYPE__MAX, "json"), "LPDDR5")

def test_lpddr4_still_labelled(self):
self.assertEqual(self._run_vram(30, "human"), "LPDDR4")

def test_non_max_type_unaffected(self):
self.assertEqual(self._run_vram(22, "human"), "GDDR6")


if __name__ == "__main__":
unittest.main()
Loading