plugins: add amd_x3d plugin for dual-CCD X3D scheduling mode
Add a new amd_x3d plugin that controls the amd_x3d_vcache kernel driver's scheduling preference on dual-CCD 3D V-Cache processors. The plugin manages the sysfs knob exposed at: /sys/bus/platform/drivers/amd_x3d_vcache/*/amd_x3d_mode and supports the two kernel-defined modes: - cache: prefer the X3D CCD for gaming and cache-sensitive workloads - frequency: prefer the non-X3D CCD for compute workloads The sysfs path is discovered via glob so the plugin works across boards with different ACPI device names. On unsupported systems, or when the driver is not present, the plugin is a no-op. State is integrated with TuneD's standard command_set/command_get flow, so the previous mode is saved and restored automatically on profile switch. Add unit tests covering discovery, apply/verify/rollback, and the no-device case. Signed-off-by: Peter Jung <admin@ptr1337.dev>
This commit is contained in:
parent
ba8ee0871c
commit
8793250857
2 changed files with 220 additions and 0 deletions
112
tests/unit/plugins/test_plugin_amd_x3d.py
Normal file
112
tests/unit/plugins/test_plugin_amd_x3d.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import tempfile
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import call
|
||||
from unittest.mock import patch
|
||||
except ImportError:
|
||||
from mock import Mock
|
||||
from mock import call
|
||||
from mock import patch
|
||||
|
||||
from tuned.monitors.repository import Repository
|
||||
from tuned.plugins.plugin_amd_x3d import AMDX3DPlugin
|
||||
from tuned.plugins.plugin_amd_x3d import _find_x3d_paths
|
||||
from tuned.utils.commands import commands
|
||||
import tuned.plugins as plugins
|
||||
import tuned.profiles as profiles
|
||||
from tuned import storage
|
||||
|
||||
|
||||
class AMDX3DPluginTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._storage_file = tempfile.NamedTemporaryFile()
|
||||
plugin_instance_factory = plugins.instance.Factory()
|
||||
storage_provider = storage.PickleProvider(self._storage_file.name)
|
||||
storage_factory = storage.Factory(storage_provider)
|
||||
|
||||
self._plugin = AMDX3DPlugin(
|
||||
Repository(),
|
||||
storage_factory,
|
||||
Mock(),
|
||||
Mock(),
|
||||
Mock(),
|
||||
plugin_instance_factory,
|
||||
None,
|
||||
profiles.variables.Variables(),
|
||||
)
|
||||
self._plugin._cmd = commands()
|
||||
self._plugin._cmd.read_file = Mock()
|
||||
self._plugin._cmd.write_to_file = Mock()
|
||||
|
||||
def tearDown(self):
|
||||
self._storage_file.close()
|
||||
|
||||
def _create_instance(self, mode="cache"):
|
||||
instance = self._plugin.create_instance(
|
||||
"amd_x3d",
|
||||
0,
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
{"mode": mode},
|
||||
)
|
||||
self._plugin.initialize_instance(instance)
|
||||
return instance
|
||||
|
||||
def test_find_x3d_paths_sorted(self):
|
||||
with patch("tuned.plugins.plugin_amd_x3d.glob.glob",
|
||||
return_value=["/sys/devices/b", "/sys/devices/a"]):
|
||||
self.assertEqual(_find_x3d_paths(),
|
||||
["/sys/devices/a", "/sys/devices/b"])
|
||||
|
||||
def test_apply_verify_and_unapply_mode(self):
|
||||
paths = [
|
||||
"/sys/bus/platform/drivers/amd_x3d_vcache/AMDI0001:00/amd_x3d_mode",
|
||||
"/sys/bus/platform/drivers/amd_x3d_vcache/AMDI0002:00/amd_x3d_mode",
|
||||
]
|
||||
instance = self._create_instance(mode="cache")
|
||||
|
||||
with patch.object(self._plugin, "_x3d_paths", return_value=paths):
|
||||
self._plugin._cmd.read_file.return_value = "frequency\n"
|
||||
instance.apply_tuning()
|
||||
|
||||
self.assertEqual(
|
||||
self._plugin._storage_get(instance, self._plugin._commands["mode"]),
|
||||
"frequency",
|
||||
)
|
||||
self._plugin._cmd.write_to_file.assert_has_calls([
|
||||
call(paths[0], "cache", no_error=False),
|
||||
call(paths[1], "cache", no_error=False),
|
||||
])
|
||||
|
||||
self._plugin._cmd.read_file.reset_mock()
|
||||
self._plugin._cmd.write_to_file.reset_mock()
|
||||
self._plugin._cmd.read_file.return_value = "frequency [cache]\n"
|
||||
self.assertTrue(instance.verify_tuning(False))
|
||||
self._plugin._cmd.write_to_file.assert_not_called()
|
||||
|
||||
self._plugin._cmd.read_file.reset_mock()
|
||||
self._plugin._cmd.write_to_file.reset_mock()
|
||||
instance.unapply_tuning()
|
||||
|
||||
self._plugin._cmd.write_to_file.assert_has_calls([
|
||||
call(paths[0], "frequency", no_error=False),
|
||||
call(paths[1], "frequency", no_error=False),
|
||||
])
|
||||
self.assertIsNone(
|
||||
self._plugin._storage_get(instance, self._plugin._commands["mode"])
|
||||
)
|
||||
|
||||
def test_apply_without_supported_device_is_noop(self):
|
||||
instance = self._create_instance(mode="cache")
|
||||
|
||||
with patch.object(self._plugin, "_x3d_paths", return_value=[]):
|
||||
instance.apply_tuning()
|
||||
|
||||
self._plugin._cmd.write_to_file.assert_not_called()
|
||||
self.assertIsNone(
|
||||
self._plugin._storage_get(instance, self._plugin._commands["mode"])
|
||||
)
|
||||
108
tuned/plugins/plugin_amd_x3d.py
Normal file
108
tuned/plugins/plugin_amd_x3d.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import glob
|
||||
import errno
|
||||
|
||||
from . import base
|
||||
from .decorators import *
|
||||
import tuned.logs
|
||||
|
||||
log = tuned.logs.get()
|
||||
|
||||
_X3D_MODE_GLOB = "/sys/bus/platform/drivers/amd_x3d_vcache/*/amd_x3d_mode"
|
||||
_VALID_MODES = frozenset(["cache", "frequency"])
|
||||
|
||||
|
||||
def _find_x3d_paths():
|
||||
"""Return discovered amd_x3d_mode sysfs paths in a stable order."""
|
||||
return sorted(glob.glob(_X3D_MODE_GLOB))
|
||||
|
||||
|
||||
class AMDX3DPlugin(base.Plugin):
|
||||
"""
|
||||
Controls the AMD 3D V-Cache scheduling mode on dual-CCD processors
|
||||
such as Ryzen 9 7950X3D, 7900X3D, 9950X3D, and 9900X3D processors.
|
||||
|
||||
The [option]`mode` option configures the `amd_x3d_vcache` kernel
|
||||
driver. The driver exposes a sysfs knob that biases the scheduler
|
||||
towards one CCD or the other:
|
||||
|
||||
* `cache`
|
||||
+
|
||||
Prefer the CCD with 3D V-Cache. This is useful for games and other
|
||||
cache-sensitive workloads.
|
||||
|
||||
* `frequency`
|
||||
+
|
||||
Prefer the non-X3D CCD, which can usually boost higher. This is
|
||||
useful for throughput-oriented compute workloads and is the kernel
|
||||
default.
|
||||
|
||||
The plugin discovers the sysfs path using a glob because the ACPI
|
||||
device name can vary across boards. On systems without the
|
||||
`amd_x3d_vcache` driver or without a supported dual-CCD X3D CPU, the
|
||||
plugin does nothing.
|
||||
|
||||
.Prefer the X3D CCD for games
|
||||
====
|
||||
----
|
||||
[amd_x3d]
|
||||
mode=cache
|
||||
----
|
||||
====
|
||||
|
||||
.Prefer the higher-frequency CCD for compute workloads
|
||||
====
|
||||
----
|
||||
[amd_x3d]
|
||||
mode=frequency
|
||||
----
|
||||
====
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _get_config_options(cls):
|
||||
return {
|
||||
"mode": None,
|
||||
}
|
||||
|
||||
def _instance_init(self, instance):
|
||||
instance._has_static_tuning = True
|
||||
instance._has_dynamic_tuning = False
|
||||
|
||||
def _instance_cleanup(self, instance):
|
||||
pass
|
||||
|
||||
def _x3d_paths(self):
|
||||
return _find_x3d_paths()
|
||||
|
||||
@command_set("mode")
|
||||
def _set_mode(self, value, instance, sim, remove):
|
||||
if value not in _VALID_MODES:
|
||||
if not sim:
|
||||
log.warning("amd_x3d: invalid mode '%s', expected one of: %s"
|
||||
% (value, ", ".join(sorted(_VALID_MODES))))
|
||||
return None
|
||||
|
||||
paths = self._x3d_paths()
|
||||
if not paths:
|
||||
if not sim:
|
||||
log.debug("amd_x3d: no AMD 3D V-Cache device found, skipping")
|
||||
return None
|
||||
|
||||
if not sim:
|
||||
for path in paths:
|
||||
log.info("amd_x3d: setting mode to '%s' on %s" % (value, path))
|
||||
self._cmd.write_to_file(path, value,
|
||||
no_error=[errno.ENOENT] if remove else False)
|
||||
return value
|
||||
|
||||
@command_get("mode")
|
||||
def _get_mode(self, instance):
|
||||
paths = self._x3d_paths()
|
||||
if not paths:
|
||||
return None
|
||||
|
||||
# All CCD pairs share the same mode; read from the first found path.
|
||||
data = self._cmd.read_file(paths[0]).strip()
|
||||
if not data:
|
||||
return None
|
||||
return self._cmd.get_active_option(data)
|
||||
Loading…
Reference in a new issue