diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 07d88e52e..f9fc624f7 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -239,11 +239,37 @@ def _get_power_from_cpu_load(self): f"CPU load {self._tdp} W and {cpu_load:.1f}% {load_factor=} => estimation of {power} W for whole machine." ) elif self._tracking_mode == "process": - - cpu_load = self._process.cpu_percent(interval=0.5) / self._cpu_count + # Get CPU usage for main process + cpu_load = self._process.cpu_percent(interval=0.5) + + # Add CPU usage from all child processes (recursive) + # This makes CPU tracking consistent with RAM tracking + try: + children = self._process.children(recursive=True) + logger.info(f"Found {len(children)} child processes") + for child in children: + try: + # Use interval=0.0 for children to avoid blocking + child_cpu = child.cpu_percent(interval=0.0) + logger.info(f"Child {child.pid} CPU: {child_cpu}") + cpu_load += child_cpu + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess, + ): + # Child process may have terminated or we don't have access + continue + except (psutil.NoSuchProcess, psutil.AccessDenied): + # Main process terminated or access denied + pass + + # Normalize by CPU count + logger.info(f"Total CPU load (all processes): {cpu_load}") + cpu_load = cpu_load / self._cpu_count power = self._tdp * cpu_load / 100 logger.debug( - f"CPU load {self._tdp} W and {cpu_load * 100:.1f}% => estimation of {power} W for process {self._pid}." + f"CPU load {self._tdp} W and {cpu_load * 100:.1f}% => estimation of {power} W for process {self._pid} (including children)." ) else: raise Exception(f"Unknown tracking_mode {self._tracking_mode}") diff --git a/tests/test_child_process_tracking.py b/tests/test_child_process_tracking.py new file mode 100644 index 000000000..70bf9fa8d --- /dev/null +++ b/tests/test_child_process_tracking.py @@ -0,0 +1,109 @@ +import multiprocessing +import os +import sys +import time + +from codecarbon import EmissionsTracker + +""" +Test script to verify that CPU tracking includes child processes +when using tracking_mode="process". + +This test creates multiple child processes that perform CPU-intensive work +and verifies that the CPU usage is properly tracked. +""" + + +def cpu_intensive_work(duration, process_id): + """Perform CPU-intensive work for the specified duration""" + print(f"Child process {process_id} starting CPU-intensive work for {duration}s") + end_time = time.time() + duration + iterations = 0 + while time.time() < end_time: + # Perform some CPU-intensive calculations + _ = sum(i * i for i in range(10000)) + iterations += 1 + print(f"Child process {process_id} completed {iterations} iterations") + + +def test_child_process_tracking(): + """Test that child processes are tracked in process mode""" + print("=" * 80) + print("Testing CPU Child Process Tracking") + print("=" * 80) + + # Start tracker in process mode + print("\nStarting EmissionsTracker in 'process' mode...") + tracker = EmissionsTracker( + tracking_mode="process", + measure_power_secs=1, + save_to_file=False, + log_level="info", + force_mode_cpu_load=True, # Force software estimation to test our fix + ) + tracker.start() + + print(f"Main process PID: {os.getpid()}") + + # Spawn multiple child processes + num_processes = 4 + work_duration = 5 # seconds + + print(f"\nSpawning {num_processes} child processes for {work_duration}s each...") + processes = [] + for i in range(num_processes): + p = multiprocessing.Process(target=cpu_intensive_work, args=(work_duration, i)) + p.start() + processes.append(p) + print(f" Started child process {i} (PID: {p.pid})") + + # Wait for children to complete + print("\nWaiting for child processes to complete...") + for i, p in enumerate(processes): + p.join() + print(f" Child process {i} completed") + + # Stop tracker and get emissions + print("\nStopping tracker...") + emissions = tracker.stop() + + # Display results + print("\n" + "=" * 80) + print("RESULTS") + print("=" * 80) + print(f"Total emissions: {emissions:.6f} kg CO2") + print(f"CPU energy: {tracker.final_emissions_data.cpu_energy:.6f} kWh") + print(f"CPU power: {tracker.final_emissions_data.cpu_power:.2f} W") + print(f"Duration: {tracker.final_emissions_data.duration:.2f} s") + + # Verify that we tracked some CPU usage + if tracker.final_emissions_data.cpu_energy > 0: + print("\nāœ“ SUCCESS: CPU energy was tracked (child processes included)") + else: + print("\nāœ— FAILURE: No CPU energy tracked") + return False + + # Calculate expected minimum energy + # With 4 child processes running CPU-intensive work for 5 seconds, + # we should see significant CPU usage + expected_min_power = 10 # Watts (conservative estimate) + if tracker.final_emissions_data.cpu_power >= expected_min_power: + print( + f"āœ“ SUCCESS: CPU power ({tracker.final_emissions_data.cpu_power:.2f}W) is above minimum threshold ({expected_min_power}W)" + ) + else: + print( + f"⚠ WARNING: CPU power ({tracker.final_emissions_data.cpu_power:.2f}W) is below expected threshold ({expected_min_power}W)" + ) + print(" This might indicate child processes are not being tracked properly") + + print("\n" + "=" * 80) + return True + + +if __name__ == "__main__": + # Set start method for multiprocessing + multiprocessing.set_start_method("spawn", force=True) + + success = test_child_process_tracking() + sys.exit(0 if success else 1)