diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 96ed00c91..34b36399b 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -501,7 +501,9 @@ def __init__( `sudo lshw -C memory -short | grep DIMM` to get RAM slots, then RAM power (W) = Number of RAM Slots × 5 Watts. :param pue: PUE (Power Usage Effectiveness) of the data center where the - experiment is being run. + experiment is being run. It multiplies both the reported power + and the reported energy, including forced values: with + `force_cpu_power=100` and `pue=1.5` the CPU is reported at 150 W. :param wue: WUE (Water Usage Effectiveness) of the data center. Units of L/kWh: litres of water consumed per kilowatt-hour of electricity consumed. :param force_carbon_intensity_g_co2e_kwh: Override grid carbon intensity @@ -1174,7 +1176,9 @@ def _do_measurements(self) -> None: power, energy, ) = hardware.measure_power_and_energy(last_duration=last_duration) - # Apply the PUE of the datacenter to the consumed energy + # Apply the PUE of the datacenter to both the power and the consumed + # energy, so the two stay consistent at the facility level + power *= self._pue energy *= self._pue water = Water.from_litres(litres=self._wue * energy.kWh) self._total_energy += energy @@ -1584,7 +1588,9 @@ def track_emissions( :param force_ram_power: Force the RAM power consumption in watts. Estimate with `sudo lshw -C memory -short | grep DIMM` for RAM slots, then RAM power (W) = Number of RAM Slots × 5 Watts. - :param pue: PUE (Power Usage Effectiveness) of the data center. + :param pue: PUE (Power Usage Effectiveness) of the data center. It multiplies both + the reported power and the reported energy, including forced values: + with `force_cpu_power=100` and `pue=1.5` the CPU is reported at 150 W. :param wue: WUE (Water Usage Effectiveness) of the data center. Units of L/kWh: litres of water consumed per kilowatt-hour of electricity consumed. :param force_carbon_intensity_g_co2e_kwh: Override grid carbon intensity diff --git a/docs/reference/output.md b/docs/reference/output.md index 720e0a883..ef5952fb0 100644 --- a/docs/reference/output.md +++ b/docs/reference/output.md @@ -65,6 +65,15 @@ The package has an in-built logger that logs data into a CSV file named `emissio | ram_utilization_percent | Average RAM utilization during tracking period (%) | | ram_used_gb | Average RAM used during tracking period (GB) | +!!! note "PUE and the power columns" + Since v3.4.0, the `pue` multiplier is applied to the reported power columns + (`cpu_power`, `gpu_power`, `ram_power`) as well as to the energy columns, so + that `energy_consumed` stays reconstructible from the reported power. Before + that, only the energy was scaled. With `pue=1.5`, a machine measured at 100 W + is reported as 150 W, and that also applies to forced values: `force_cpu_power=100` + with `pue=1.5` reports a 150 W CPU. This is intended: the columns describe the + power drawn at the facility level, not at the socket. + !!! note Developers can enhance the Output interface by implementing a custom class that extends `BaseOutput` at `codecarbon/output.py`. For example, to log into a database. diff --git a/tests/test_emissions_tracker_constant.py b/tests/test_emissions_tracker_constant.py index 724cbe9ab..de2791dc7 100644 --- a/tests/test_emissions_tracker_constant.py +++ b/tests/test_emissions_tracker_constant.py @@ -88,6 +88,39 @@ def test_carbon_tracker_offline_constant_force_cpu_power( assertdf = pd.read_csv(self.emissions_file_path) self.assertEqual(USER_INPUT_CPU_POWER / 2, assertdf["cpu_power"][0]) + @mock.patch.object(cpu.TDP, "_get_cpu_power_from_registry") + @mock.patch.object(cpu, "is_psutil_available") + def test_carbon_tracker_offline_constant_pue(self, mock_tdp, mock_psutil): + # The PUE must be applied to the reported power as well as to the energy, + # so that energy_consumed stays consistent with the power columns. + USER_INPUT_CPU_POWER = 1_000 + PUE = 2.0 + mock_tdp.return_value = None + mock_psutil.return_value = False + tracker = OfflineEmissionsTracker( + country_iso_code="USA", + output_dir=self.emissions_path, + output_file=self.emissions_file, + force_cpu_power=USER_INPUT_CPU_POWER, + pue=PUE, + ) + tracker.start() + heavy_computation(run_time_secs=1) + emissions = tracker.stop() + assert isinstance(emissions, float) + assertdf = pd.read_csv(self.emissions_file_path) + self.assertEqual(USER_INPUT_CPU_POWER / 2 * PUE, assertdf["cpu_power"][0]) + # energy_consumed must be reconstructible from the reported power + total_power = ( + assertdf["cpu_power"][0] + + assertdf["gpu_power"][0] + + assertdf["ram_power"][0] + ) + expected_energy = total_power * assertdf["duration"][0] / 3600 / 1000 + self.assertAlmostEqual( + expected_energy, assertdf["energy_consumed"][0], delta=expected_energy * 0.1 + ) + @mock.patch("codecarbon.external.hardware.psutil.cpu_percent", return_value=50.0) @mock.patch.object(cpu.TDP, "_get_cpu_power_from_registry") @mock.patch.object(cpu, "is_psutil_available")