[PATCH 4/7] rteval: Simplify cpuset implementation to match isolcpus philosophy

John Kacur <[email protected]> Thu, 30 Apr 2026 15:31:41 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Simplify the cpuset integration to create only 1-2 cpusets instead of 1-3,
matching how rteval works with isolcpus: measurements get isolation, loads
use affinity constraints only.

Changes:
- rteval_housekeeping: Created only if --housekeeping specified (system tasks)
- rteval_measurement: Always created for measurement threads
- Loads: Use taskset for CPU affinity (no cpuset migration)

This approach:
- Mirrors isolcpus behavior (measurements isolated, loads constrained via affinity)
- Eliminates complexity of load cpuset migration
- Removes race conditions with short-lived load processes
- Simplifies codebase while maintaining CPU isolation goals

Modified files:
- rteval/cpusetmanager.py: Removed load_cpus parameter and migrate_load_threads()
- rteval-cmd: Updated CpusetManager instantiation, removed load migration
- rteval/__init__.py: Simplified to only migrate measurement threads
- rteval/modules/loads/*: Removed get_subprocess_pids() methods (no longer needed)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Signed-off-by: John Kacur <[email protected]>
---
 rteval-cmd                        |   8 +-
 rteval/__init__.py                |  11 +--
 rteval/cpusetmanager.py           | 131 ++++++------------------------
 rteval/modules/loads/hackbench.py |   8 --
 rteval/modules/loads/kcompile.py  |   8 --
 rteval/modules/loads/stressng.py  |   6 --
 6 files changed, 30 insertions(+), 142 deletions(-)

diff --git a/rteval-cmd b/rteval-cmd
index 85adf2502d20..29254a8f1a7c 100755
--- a/rteval-cmd
+++ b/rteval-cmd
@@ -542,10 +542,10 @@ if __name__ == '__main__':
             CpusetManager.cleanup_leftover_cpusets(logger)
 
             # Create manager (will create cpusets in __enter__)
+            # Note: Load CPUs not needed - loads use taskset for CPU affinity
             cpuset_manager = CpusetManager(
                 housekeeping_cpus=housekeeping_cpus,
                 measurement_cpus=msrcfg_cpus,
-                load_cpus=ldcfg_cpus,
                 logger=logger
             )
 
@@ -568,11 +568,7 @@ if __name__ == '__main__':
                     loadmods.Start()
                     nthreads = loadmods.Unleash()
 
-                    # Migrate load processes
-                    time.sleep(0.5)  # Give processes time to spawn
-                    load_pids = loadmods.GetSubprocessPids()
-                    cpuset_manager.migrate_load_threads(load_pids)
-
+                    # Loads use taskset for CPU affinity (no cpuset migration needed)
                     logger.log(Log.INFO, f"Started {nthreads} load threads - will run for {rtevcfg.duration} seconds")
                     logger.log(Log.INFO, "No measurements will be performed, due to the --onlyload option")
                     time.sleep(rtevcfg.duration)
diff --git a/rteval/__init__.py b/rteval/__init__.py
index a85fe2e21328..2eadb2045e2b 100644
--- a/rteval/__init__.py
+++ b/rteval/__init__.py
@@ -185,20 +185,15 @@ class RtEval(rtevalReport):
             self._measuremods.Unleash()
             measure_start = datetime.now()
 
-            # After Unleash(), do cpuset migration
+            # After Unleash(), migrate measurement threads to cpuset
+            # Note: Load threads use taskset for CPU affinity (no cpuset migration needed)
             if self._cpuset_manager:
-                # Small delay to let processes spawn
+                # Small delay to let measurement processes spawn
                 time.sleep(0.5)
 
-                if self._loadmods:
-                    load_pids = self._loadmods.GetSubprocessPids()
-                    self._cpuset_manager.migrate_load_threads(load_pids)
-
                 meas_pids = self._measuremods.GetSubprocessPids()
                 self._cpuset_manager.migrate_measurement_threads(meas_pids)
 
-                self.__logger.log(Log.INFO, "Processes migrated to cpusets")
-
             # wait for time to expire or thread to die
             signal.signal(signal.SIGINT, sig_handler)
             signal.signal(signal.SIGTERM, sig_handler)
diff --git a/rteval/cpusetmanager.py b/rteval/cpusetmanager.py
index 55f57580d5fa..c1aa04431f3a 100644
--- a/rteval/cpusetmanager.py
+++ b/rteval/cpusetmanager.py
@@ -20,10 +20,11 @@ class CpusetManager:
     """
     Manager for rteval cpusets with automatic cleanup
 
-    Creates 1-3 cpusets dynamically based on CPU configuration:
+    Creates 1-2 cpusets based on configuration:
     - rteval_housekeeping: Only if housekeeping_cpus specified
-    - rteval_workload: When measurement_cpus == load_cpus (combined)
-    - rteval_measurement + rteval_loads: When measurement_cpus != load_cpus (separate)
+    - rteval_measurement: Always created for measurement workloads
+
+    Load workloads use taskset for CPU affinity (no cpuset needed).
 
     Uses context manager pattern for automatic cleanup.
     """
@@ -69,15 +70,16 @@ class CpusetManager:
             except Exception as e:
                 logger.log(Log.WARN, f"Failed to clean up {cpuset_name}: {e}")
 
-    def __init__(self, housekeeping_cpus, measurement_cpus, load_cpus, logger):
+    def __init__(self, housekeeping_cpus, measurement_cpus, logger):
         """
         Initialize cpuset manager
 
         Args:
             housekeeping_cpus: List of CPU integers for housekeeping (may be empty)
             measurement_cpus: List of CPU integers for measurement workloads
-            load_cpus: List of CPU integers for load workloads
             logger: rteval Log instance for logging
+
+        Note: Load workloads use taskset for CPU affinity and don't need cpusets.
         """
         # Check cpuset support
         self.cpusets_init = CpusetsInit()
@@ -87,26 +89,18 @@ class CpusetManager:
         # Store parameters
         self.housekeeping_cpus = housekeeping_cpus
         self.measurement_cpus = measurement_cpus
-        self.load_cpus = load_cpus
         self.logger = logger
 
         # Cpuset objects (will be created in __enter__)
         self.housekeeping_cpuset = None
         self.measurement_cpuset = None
-        self.loads_cpuset = None
-        self.workload_cpuset = None
-
-        # Determine cpuset creation strategy
-        self.use_workload_cpuset = (measurement_cpus == load_cpus)
 
         # Get NUMA node range for memory assignment
         self.numa_nodes = f"0-{self.cpusets_init.numa_nodes - 1}" if self.cpusets_init.numa_nodes > 1 else "0"
 
         self.logger.log(Log.DEBUG, f"CpusetManager initialized: "
                        f"housekeeping={collapse_cpulist(housekeeping_cpus) if housekeeping_cpus else 'none'}, "
-                       f"measurement={collapse_cpulist(measurement_cpus)}, "
-                       f"loads={collapse_cpulist(load_cpus)}, "
-                       f"strategy={'combined workload' if self.use_workload_cpuset else 'separate'}")
+                       f"measurement={collapse_cpulist(measurement_cpus)}")
 
     def __enter__(self):
         """
@@ -125,27 +119,12 @@ class CpusetManager:
             self.housekeeping_cpuset.assign_cpus(collapse_cpulist(self.housekeeping_cpus))
             self.housekeeping_cpuset.write_cpu_exclusive(False)  # partition=member
 
-        # Create workload cpusets based on strategy
-        if self.use_workload_cpuset:
-            # Same CPUs for measurement and loads - create single combined cpuset
-            self.logger.log(Log.DEBUG, f"Creating rteval_workload cpuset with CPUs {collapse_cpulist(self.measurement_cpus)}")
-            self.workload_cpuset = Cpuset('rteval_workload')
-            self.workload_cpuset.write_memnode(self.numa_nodes)
-            self.workload_cpuset.assign_cpus(collapse_cpulist(self.measurement_cpus))
-            self.workload_cpuset.write_cpu_exclusive(False)  # partition=member
-        else:
-            # Different CPUs - create separate measurement and loads cpusets
-            self.logger.log(Log.DEBUG, f"Creating rteval_measurement cpuset with CPUs {collapse_cpulist(self.measurement_cpus)}")
-            self.measurement_cpuset = Cpuset('rteval_measurement')
-            self.measurement_cpuset.write_memnode(self.numa_nodes)
-            self.measurement_cpuset.assign_cpus(collapse_cpulist(self.measurement_cpus))
-            self.measurement_cpuset.write_cpu_exclusive(False)  # partition=member
-
-            self.logger.log(Log.DEBUG, f"Creating rteval_loads cpuset with CPUs {collapse_cpulist(self.load_cpus)}")
-            self.loads_cpuset = Cpuset('rteval_loads')
-            self.loads_cpuset.write_memnode(self.numa_nodes)
-            self.loads_cpuset.assign_cpus(collapse_cpulist(self.load_cpus))
-            self.loads_cpuset.write_cpu_exclusive(False)  # partition=member
+        # Create measurement cpuset
+        self.logger.log(Log.DEBUG, f"Creating rteval_measurement cpuset with CPUs {collapse_cpulist(self.measurement_cpus)}")
+        self.measurement_cpuset = Cpuset('rteval_measurement')
+        self.measurement_cpuset.write_memnode(self.numa_nodes)
+        self.measurement_cpuset.assign_cpus(collapse_cpulist(self.measurement_cpus))
+        self.measurement_cpuset.write_cpu_exclusive(False)  # partition=member
 
         self.logger.log(Log.INFO, "Cpusets created successfully")
         return self
@@ -165,21 +144,10 @@ class CpusetManager:
             # Move all processes back to root cgroup before destroying cpusets
             # Destroy in reverse order of creation
 
-            if self.use_workload_cpuset:
-                if self.workload_cpuset:
-                    self._migrate_to_root(self.workload_cpuset, 'rteval_workload')
-                    self.workload_cpuset.destroy()
-                    self.logger.log(Log.DEBUG, "Destroyed rteval_workload cpuset")
-            else:
-                if self.loads_cpuset:
-                    self._migrate_to_root(self.loads_cpuset, 'rteval_loads')
-                    self.loads_cpuset.destroy()
-                    self.logger.log(Log.DEBUG, "Destroyed rteval_loads cpuset")
-
-                if self.measurement_cpuset:
-                    self._migrate_to_root(self.measurement_cpuset, 'rteval_measurement')
-                    self.measurement_cpuset.destroy()
-                    self.logger.log(Log.DEBUG, "Destroyed rteval_measurement cpuset")
+            if self.measurement_cpuset:
+                self._migrate_to_root(self.measurement_cpuset, 'rteval_measurement')
+                self.measurement_cpuset.destroy()
+                self.logger.log(Log.DEBUG, "Destroyed rteval_measurement cpuset")
 
             if self.housekeeping_cpuset:
                 self._migrate_to_root(self.housekeeping_cpuset, 'rteval_housekeeping')
@@ -229,76 +197,27 @@ class CpusetManager:
 
     def migrate_measurement_threads(self, pids):
         """
-        Migrate measurement process PIDs to appropriate cpuset
-
-        Migrates to rteval_measurement if using separate cpusets,
-        or rteval_workload if using combined cpuset.
+        Migrate measurement subprocess PIDs to rteval_measurement cpuset
 
         Args:
-            pids: List of process IDs to migrate
+            pids: List of subprocess PIDs to migrate
         """
         if not pids:
             self.logger.log(Log.DEBUG, "No measurement PIDs to migrate")
             return
 
-        # Determine target cpuset
-        if self.use_workload_cpuset:
-            target_cpuset = self.workload_cpuset
-            cpuset_name = "rteval_workload"
-        else:
-            target_cpuset = self.measurement_cpuset
-            cpuset_name = "rteval_measurement"
-
-        if not target_cpuset:
-            self.logger.log(Log.WARN, f"Target cpuset {cpuset_name} not created, cannot migrate measurement threads")
-            return
-
-        self.logger.log(Log.DEBUG, f"Migrating {len(pids)} measurement PIDs to {cpuset_name}")
-
-        migrated = 0
-        failed = 0
-        for pid in pids:
-            if target_cpuset.write_pid(pid):
-                migrated += 1
-            else:
-                failed += 1
-
-        self.logger.log(Log.INFO, f"Migrated {migrated} measurement threads to {cpuset_name} (failed: {failed})")
-
-    def migrate_load_threads(self, pids):
-        """
-        Migrate load process PIDs to appropriate cpuset
-
-        Migrates to rteval_loads if using separate cpusets,
-        or rteval_workload if using combined cpuset.
-
-        Args:
-            pids: List of process IDs to migrate
-        """
-        if not pids:
-            self.logger.log(Log.DEBUG, "No load PIDs to migrate")
-            return
-
-        # Determine target cpuset
-        if self.use_workload_cpuset:
-            target_cpuset = self.workload_cpuset
-            cpuset_name = "rteval_workload"
-        else:
-            target_cpuset = self.loads_cpuset
-            cpuset_name = "rteval_loads"
-
-        if not target_cpuset:
-            self.logger.log(Log.WARN, f"Target cpuset {cpuset_name} not created, cannot migrate load threads")
+        if not self.measurement_cpuset:
+            self.logger.log(Log.WARN, "rteval_measurement cpuset not created, cannot migrate measurement threads")
             return
 
-        self.logger.log(Log.DEBUG, f"Migrating {len(pids)} load PIDs to {cpuset_name}")
+        self.logger.log(Log.DEBUG, f"Migrating {len(pids)} measurement PIDs to rteval_measurement")
 
         migrated = 0
         failed = 0
         for pid in pids:
-            if target_cpuset.write_pid(pid):
+            if self.measurement_cpuset.write_pid(pid):
                 migrated += 1
             else:
                 failed += 1
 
-        self.logger.log(Log.INFO, f"Migrated {migrated} load threads to {cpuset_name} (failed: {failed})")
+        self.logger.log(Log.INFO, f"Migrated {migrated} measurement threads to rteval_measurement (failed: {failed})")
diff --git a/rteval/modules/loads/hackbench.py b/rteval/modules/loads/hackbench.py
index d04231de384f..946a2adc2e57 100644
--- a/rteval/modules/loads/hackbench.py
+++ b/rteval/modules/loads/hackbench.py
@@ -159,14 +159,6 @@ class Hackbench(CommandLineLoad):
         # As hackbench is short-lived, lets pretend it is always alive
         return True
 
-    def get_subprocess_pids(self):
-        """Return PIDs of all running hackbench processes"""
-        pids = []
-        for task in self.tasks.values():
-            if task and task.poll() is None:
-                pids.append(task.pid)
-        return pids
-
     def _WorkloadCleanup(self):
         if self._donotrun:
             return
diff --git a/rteval/modules/loads/kcompile.py b/rteval/modules/loads/kcompile.py
index 5efb0a659843..23586b7a3c8d 100644
--- a/rteval/modules/loads/kcompile.py
+++ b/rteval/modules/loads/kcompile.py
@@ -314,14 +314,6 @@ class Kcompile(CommandLineLoad):
 
         return True
 
-    def get_subprocess_pids(self):
-        """Return PIDs of all kcompile build jobs"""
-        pids = []
-        for job in self.buildjobs.values():
-            if job.jobid and job.isrunning():
-                pids.append(job.jobid.pid)
-        return pids
-
     def _WorkloadCleanup(self):
         if self._donotrun:
             return
diff --git a/rteval/modules/loads/stressng.py b/rteval/modules/loads/stressng.py
index 509217ac7f8d..4ad7197fc590 100644
--- a/rteval/modules/loads/stressng.py
+++ b/rteval/modules/loads/stressng.py
@@ -110,12 +110,6 @@ class Stressng(CommandLineLoad):
             return self.process.poll() is None
         return False
 
-    def get_subprocess_pids(self):
-        """Return PID of stress-ng process"""
-        if self.started and self.process and self.process.poll() is None:
-            return [self.process.pid]
-        return []
-
     def _WorkloadCleanup(self):
         " Makesure to kill stress-ng before rteval ends "
         if not self.started:
-- 
2.54.0