1
0
Fork 0

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:
Peter Jung 2026-03-07 17:02:54 +01:00
parent ba8ee0871c
commit 8793250857
No known key found for this signature in database
GPG key ID: C3C4820857F654FE
2 changed files with 220 additions and 0 deletions

View 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"])
)

View 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)