From e20b96372777f76324473b008fbeb2b3c24a64aa Mon Sep 17 00:00:00 2001 From: JargusB Date: Sun, 5 Apr 2026 18:00:23 -0400 Subject: [PATCH 01/29] added a function to hardware.py for cpu temp tracking, edited emissions tracker to handle gpu and cpu temps, edited emissions_data.py to export this data to the csv on output --- codecarbon/emissions_tracker.py | 39 +++++++++++++++++---- codecarbon/external/hardware.py | 37 +++++++++++++++++++ codecarbon/output_methods/emissions_data.py | 4 +++ codecarbon/test_temp.py | 37 +++++++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 codecarbon/test_temp.py diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 862eba2b4..f9ce5ee6d 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -368,6 +368,8 @@ def __init__( self._gpu_utilization_history: List[float] = [] self._ram_utilization_history: List[float] = [] self._ram_used_history: List[float] = [] + self._cpu_temperature_history: List[float] = [] + self._gpu_temperature_history: List[float] = [] self._total_cpu_energy: Energy = Energy.from_energy(kWh=0) self._total_gpu_energy: Energy = Energy.from_energy(kWh=0) self._total_ram_energy: Energy = Energy.from_energy(kWh=0) @@ -548,6 +550,8 @@ def start(self) -> None: self._ram_utilization_history.clear() self._ram_used_history.clear() self._gpu_utilization_history.clear() + self._cpu_temperature_history.clear() + self._gpu_temperature_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -598,6 +602,8 @@ def start_task(self, task_name=None) -> None: self._ram_utilization_history.clear() self._ram_used_history.clear() self._gpu_utilization_history.clear() + self._cpu_temperature_history.clear() + self._gpu_temperature_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -922,6 +928,16 @@ def _prepare_emissions_data(self) -> EmissionsData: tracking_mode=self._conf.get("tracking_mode"), pue=self._pue, wue=self._wue, + cpu_temperature=( + sum(self._cpu_temperature_history) / len(self._cpu_temperature_history) + if self._cpu_temperature_history + else 0.0 + ), + gpu_temperature=( + sum(self._gpu_temperature_history) / len(self._gpu_temperature_history) + if self._gpu_temperature_history + else 0.0 + ), ) logger.debug(total_emissions) return total_emissions @@ -973,6 +989,10 @@ def _monitor_power(self) -> None: self._ram_utilization_history.append(psutil.virtual_memory().percent) self._ram_used_history.append(psutil.virtual_memory().used / (1024**3)) + for hardware in self._hardware: + if isinstance(hardware, CPU): + self._cpu_temperature_history.append(hardware.get_cpu_temperature()) + # Collect GPU utilization metrics for hardware in self._hardware: if isinstance(hardware, GPU): @@ -980,13 +1000,18 @@ def _monitor_power(self) -> None: gpu_details = hardware.devices.get_gpu_details() for gpu_index, gpu_detail in enumerate(gpu_details): resolved_gpu_index = gpu_detail.get("gpu_index", gpu_index) - if ( - resolved_gpu_index in gpu_ids_to_monitor - and "gpu_utilization" in gpu_detail - ): - self._gpu_utilization_history.append( - gpu_detail["gpu_utilization"] - ) + if resolved_gpu_index in gpu_ids_to_monitor: + + if "gpu_utilization" in gpu_detail: + self._gpu_utilization_history.append( + gpu_detail["gpu_utilization"] + ) + + if "temperature" in gpu_detail: + self._gpu_temperature_history.append( + gpu_detail["temperature"] + ) + def _do_measurements(self) -> None: for hardware in self._hardware: diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 8ac4de8f8..3aad344c2 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -409,9 +409,46 @@ def monitor_power(self): cpu_power = self._get_power_from_cpus() self._power_history.append(cpu_power) + + def get_cpu_temperature(self) -> float: + """ + Get average CPU temperature in Celsius. + Supported on Linux (Intel + AMD) and Windows Intel via Power Gadget. + Returns 0.0 if temperature cannot be read on the current platform. + """ + try: + if self._mode == "intel_power_gadget": + all_cpu_details = self._intel_interface.get_cpu_details() + for metric, value in all_cpu_details.items(): + if re.match(r"^CPU Temperature", metric): + return float(value) + return 0.0 + + elif self._mode in ["intel_rapl", MODE_CPU_LOAD, "constant"]: + temps = psutil.sensors_temperatures() + if not temps: + logger.debug( + "get_cpu_temperature: psutil.sensors_temperatures() " + "returned no data on this platform" + ) + return 0.0 + for key in ["coretemp", "k10temp", "cpu_thermal"]: + if key in temps: + readings = temps[key] + avg = sum(r.current for r in readings) / len(readings) + logger.debug( + f"get_cpu_temperature: {key} avg = {avg:.1f}°C" + ) + return avg + return 0.0 + + except Exception as e: + logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") + return 0.0 def get_model(self): return self._model + @classmethod def from_utils( cls, diff --git a/codecarbon/output_methods/emissions_data.py b/codecarbon/output_methods/emissions_data.py index 17544aa51..b086f3eca 100644 --- a/codecarbon/output_methods/emissions_data.py +++ b/codecarbon/output_methods/emissions_data.py @@ -47,6 +47,8 @@ class EmissionsData: on_cloud: str = "N" pue: float = 1 wue: float = 0 + cpu_temperature: float = 0.0 # ADD + gpu_temperature: float = 0.0 # ADD @property def values(self) -> OrderedDict: @@ -110,6 +112,8 @@ class TaskEmissionsData: ram_utilization_percent: float = 0.0 ram_used_gb: float = 0.0 on_cloud: str = "N" + cpu_temperature: float = 0.0 + gpu_temperature: float = 0.0 @property def values(self) -> OrderedDict: diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py new file mode 100644 index 000000000..db5bac723 --- /dev/null +++ b/codecarbon/test_temp.py @@ -0,0 +1,37 @@ +# test_temp.py +import time +import pandas as pd +from codecarbon import EmissionsTracker + +tracker = EmissionsTracker( + project_name="temperature_test", + measure_power_secs=15, + save_to_file=True, + output_file="emissions_temp_test.csv", + log_level="debug" +) + +tracker.start() + +# simulate some work +print("Running workload...") +total = 0 +for i in range(10_000_000): + total += i + +time.sleep(30) # give monitor_power time to collect samples + +emissions = tracker.stop() + +# check results +print(f"\n--- Results ---") +print(f"Emissions: {emissions:.6f} kg CO2") +print(f"CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") +print(f"GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") + +# verify CSV +df = pd.read_csv("emissions_temp_test.csv") +print(f"\n--- CSV columns ---") +print(df.columns.tolist()) +print(f"\n--- Temperature values in CSV ---") +print(df[["cpu_temperature", "gpu_temperature"]]) From db8360f176c93033bc4299f3b5261ca4b4dc81d4 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:33:24 -0400 Subject: [PATCH 02/29] added temp tracking test workflow --- .github/workflows/test_temp.yml | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/test_temp.yml diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml new file mode 100644 index 000000000..320e78431 --- /dev/null +++ b/.github/workflows/test_temp.yml @@ -0,0 +1,58 @@ +name: Test Temperature Tracking + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + test-temperature: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -e . + pip install pandas + + - name: Check sensors available + run: | + sudo apt-get install -y lm-sensors + python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" + + - name: Run temperature test + run: | + python3 -c " + import time + from codecarbon import EmissionsTracker + + tracker = EmissionsTracker( + project_name='temperature_test', + measure_power_secs=15, + save_to_file=True, + output_file='emissions_temp_test.csv', + log_level='debug' + ) + + tracker.start() + total = sum(range(10_000_000)) + time.sleep(30) + emissions = tracker.stop() + + print(f'Emissions: {emissions:.6f} kg CO2') + print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') + print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') + + import pandas as pd + df = pd.read_csv('emissions_temp_test.csv') + print('CSV columns:', df.columns.tolist()) + print('Temperature values:') + print(df[['cpu_temperature', 'gpu_temperature']]) + " From 29a0c9ebe1c46e5db236f32759987faa90984fd0 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:38:11 -0400 Subject: [PATCH 03/29] Fix flake8 f-string warnings in test file --- codecarbon/test_temp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py index db5bac723..f2ac686d2 100644 --- a/codecarbon/test_temp.py +++ b/codecarbon/test_temp.py @@ -24,14 +24,14 @@ emissions = tracker.stop() # check results -print(f"\n--- Results ---") -print(f"Emissions: {emissions:.6f} kg CO2") -print(f"CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") -print(f"GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") +print("\n--- Results ---") +print("Emissions: {emissions:.6f} kg CO2") +print("CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") +print("GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") # verify CSV df = pd.read_csv("emissions_temp_test.csv") -print(f"\n--- CSV columns ---") +print("\n--- CSV columns ---") print(df.columns.tolist()) -print(f"\n--- Temperature values in CSV ---") +print("\n--- Temperature values in CSV ---") print(df[["cpu_temperature", "gpu_temperature"]]) From b60e032221be8f108aaf07afce8f36e58e7bf6b4 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:45:47 -0400 Subject: [PATCH 04/29] Apply black and isort formatting fixes --- codecarbon/emissions_tracker.py | 1 - codecarbon/external/hardware.py | 7 ++----- codecarbon/test_temp.py | 4 +++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index f9ce5ee6d..d2b43c81f 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -1012,7 +1012,6 @@ def _monitor_power(self) -> None: gpu_detail["temperature"] ) - def _do_measurements(self) -> None: for hardware in self._hardware: h_time = time.perf_counter() diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 3aad344c2..fa0ac50cf 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -409,7 +409,6 @@ def monitor_power(self): cpu_power = self._get_power_from_cpus() self._power_history.append(cpu_power) - def get_cpu_temperature(self) -> float: """ Get average CPU temperature in Celsius. @@ -436,19 +435,17 @@ def get_cpu_temperature(self) -> float: if key in temps: readings = temps[key] avg = sum(r.current for r in readings) / len(readings) - logger.debug( - f"get_cpu_temperature: {key} avg = {avg:.1f}°C" - ) + logger.debug(f"get_cpu_temperature: {key} avg = {avg:.1f}°C") return avg return 0.0 except Exception as e: logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") return 0.0 + def get_model(self): return self._model - @classmethod def from_utils( cls, diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py index f2ac686d2..38377f555 100644 --- a/codecarbon/test_temp.py +++ b/codecarbon/test_temp.py @@ -1,6 +1,8 @@ # test_temp.py import time + import pandas as pd + from codecarbon import EmissionsTracker tracker = EmissionsTracker( @@ -8,7 +10,7 @@ measure_power_secs=15, save_to_file=True, output_file="emissions_temp_test.csv", - log_level="debug" + log_level="debug", ) tracker.start() From 5e637639b81fff2a26f1718996c425cb1d78835f Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 14:00:50 -0400 Subject: [PATCH 05/29] Add cpu_temperature and gpu_temperature to test data CSV --- tests/test_data/emissions_valid_headers.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_data/emissions_valid_headers.csv b/tests/test_data/emissions_valid_headers.csv index b7493c902..aaf9ad182 100644 --- a/tests/test_data/emissions_valid_headers.csv +++ b/tests/test_data/emissions_valid_headers.csv @@ -1,2 +1,2 @@ -timestamp,project_name,run_id,experiment_id,duration,emissions,emissions_rate,cpu_power,gpu_power,ram_power,cpu_energy,gpu_energy,ram_energy,energy_consumed,water_consumed,country_name,country_iso_code,region,cloud_provider,cloud_region,os,python_version,codecarbon_version,cpu_count,cpu_model,gpu_count,gpu_model,longitude,latitude,ram_total_size,tracking_mode,cpu_utilization_percent,gpu_utilization_percent,ram_utilization_percent,ram_used_gb,on_cloud,pue,wue +timestamp,project_name,run_id,experiment_id,duration,emissions,emissions_rate,cpu_power,gpu_power,ram_power,cpu_energy,gpu_energy,ram_energy,energy_consumed,water_consumed,country_name,country_iso_code,region,cloud_provider,cloud_region,os,python_version,codecarbon_version,cpu_count,cpu_model,gpu_count,gpu_model,longitude,latitude,ram_total_size,tracking_mode,cpu_utilization_percent,gpu_utilization_percent,ram_utilization_percent,ram_used_gb,on_cloud,pue,wue,cpu_temperature,gpu_temperature 2021-09-23T15:04:51,codecarbon,0a578547-1d6b-4e2f-be0c-7ad10f2f7c97,test,161.20380687713623,0.0004490989249167,0.0027859076880178,0.269999999999999,0.0,12.884901888000002,0.0,0,0.00057442898176,0.00057442898176,0.1,Morocco,MAR,casablanca-settat,,,macOS-10.15.7-x86_64-i386-64bit,3.8.0,2.1.3,12,Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz,,,-7.9084,33.5932,,machine,0.0,0.0,0.0,0.0,N,1.0,0.0 From 528a155ee0c3681f952a5ee6f51b7b8660225ab8 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 14:11:28 -0400 Subject: [PATCH 06/29] changed test_temp workflow naming from main to master --- .github/workflows/test_temp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml index 320e78431..b9fa9a409 100644 --- a/.github/workflows/test_temp.yml +++ b/.github/workflows/test_temp.yml @@ -2,7 +2,7 @@ name: Test Temperature Tracking on: push: - branches: [ main ] + branches: [master] workflow_dispatch: jobs: From a8ebc370b2c03fc96de9384bf1623791157ceea4 Mon Sep 17 00:00:00 2001 From: Pat3690 <87405996+Pat3690@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:06:22 -0400 Subject: [PATCH 07/29] Added Documentation, can be found in /docs/Contributions --- docs/Contributions/cputemp.md | 46 ++++++++++++++++++++++++++++++ docs/images/CpuTemp.png | Bin 0 -> 8339 bytes requirements/requirements-api.txt | 8 ++++-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 docs/Contributions/cputemp.md create mode 100644 docs/images/CpuTemp.png diff --git a/docs/Contributions/cputemp.md b/docs/Contributions/cputemp.md new file mode 100644 index 000000000..8a4574e9f --- /dev/null +++ b/docs/Contributions/cputemp.md @@ -0,0 +1,46 @@ +# Contributions + +Added a function in Hardware.py that tracks cpu temps live in Celsius, this covers issue 1008 + +### Added code + +``` python +def get_cpu_temperature(self) -> float: + """ + Get average CPU temperature in Celsius. + Supported on Linux (Intel + AMD) and Windows Intel via Power Gadget. + Returns 0.0 if temperature cannot be read on the current platform. + """ + try: + if self._mode == "intel_power_gadget": + all_cpu_details = self._intel_interface.get_cpu_details() + for metric, value in all_cpu_details.items(): + if re.match(r"^CPU Temperature", metric): + return float(value) + return 0.0 + + elif self._mode in ["intel_rapl", MODE_CPU_LOAD, "constant"]: + temps = psutil.sensors_temperatures() + if not temps: + logger.debug( + "get_cpu_temperature: psutil.sensors_temperatures() " + "returned no data on this platform" + ) + return 0.0 + for key in ["coretemp", "k10temp", "cpu_thermal"]: + if key in temps: + readings = temps[key] + avg = sum(r.current for r in readings) / len(readings) + logger.debug(f"get_cpu_temperature: {key} avg = {avg:.1f}°C") + return avg + return 0.0 + + except Exception as e: + logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") + return 0.0 +``` + +Allowed for CodeCarbon to track it and input it in to the CSV data set, shown in terminal below +![](../images/CpuTemp.png){.align-center width="700px" height="400px"} + +Make sure to run the 'test_temp.py' file diff --git a/docs/images/CpuTemp.png b/docs/images/CpuTemp.png new file mode 100644 index 0000000000000000000000000000000000000000..463dc24f745acd40041fa882d4678f2b8a785fff GIT binary patch literal 8339 zcmbt)Wl&t*x+Z}HcWpc*SRhE_?oO~^AvgqgZJ?0^{qPXn-5O~uxD(vn-O@-Cw6RVu zIk!&L%+$=OTQyZ%pZCw&d+m2UQae&pT@eqP3L6Cl1y5N??mY_1v$UscV@&j?W15}o z!P5!LRms2u1qJ`d-~CxKJ3jRjh~p{$!PC*j#?I2*@dJtg4<7t8^a;lMUtj|iARiDY z1+D}>6)b^b#Ll8Ah4msy3iGQR`O9ah)Q-D+qKF9}_*k}RPaE&U4m=u(cB8Q*2%b0z zZLX2d`eCuzyiQxL&t{dUuyq=#i&;WLj2wbT<$E zMof`g_BxbM<}Hs`Is3~{LJQw-&!6slSr~gT8A5+yDRawUGWg37#}LY3=6?O3mUiFk zOJUY>XPB~K1YM9H&ns{~4%*QfCKXqIBb@XJAbB^&`TMT0N{;$qqs-`3{}yR_##8Mt zDJY^QSSu35LdoZf3>1w!vy()zZB==HE_vCv=86jsoI2GIzHWp1?)b+($OCs=_{_NP zb`im+Wrikcur0cEW(Wh8}Hd^(yR zZ$3M>wdD{Q-u*ljoq{6MCr1@E*N^433ziY}^!0WPp_@Slk*8pqrqLdUOtXgVkJZM2 zAN1XX{UyCdy`pFC9p0D}-5)3ximP^rFJDXWriu-`>Dr95EU&09GP=??-ZzS;&m&B! zP0Wr|H7<40_%6A>N1rJaftCt?psl59Ne9KBe477&Mo_(GJ_nH3gDc>7KrN6Bq2!=C zJxiS1ty7NY2=S8k25K80Hflk#bMu7h8O zZK*$XrIL|Fe_mg;T!~j+LD;}oASaah8v*RnwKmSVdc^8trW{1n-;z9h(5-*8P zQg(i7&r|iNC->6)qu}Bhta3d|;Zxp(k11Lcm(iX%bhg6%+1wu5<4%Z9tBaVFLFQye z^0S{Qfm9=0ZodN&S63+|7+)Kl=)nTaB(z5nJyupV^>K!&HYKdMu!y@`CGwu5IRwQ; z<{tyh&()%~Vt+*Abk*RPJotZtK_rGk_b!Ka)6A)Yl>wQXG*Jad(UYvFGwqeRHmBFN z*>}*xK2tK1RiAH?S;ca66P z5BnqkfZtI|KzXQQ{mvXOTYk@PvttXhcNn1e^-b%C?glIq21rM~vMqP+ZTCOLr~#n3 zi$Rs-$lCJBEYAkS_DB>1P$j88O*v5l8H*HWw7iovBqS9KG83)}WKUGMQQ&uHu`a(h zs^zXq+&Ob4`e1d6X|nDh{(w5m%DN#XGqeZL^I8GWemBeI zPquM5&C@WI_O~F+s1Bi0ka1+K-tb=O-1KN29cWkN&ZxHJyEFIU#IuWwv^nsn0JroP z!PthW>;7q2^tiuLvu7dY>qy7vkBUdwB$6OMJ9KEPwgKSBgA(w9AS0IUwUGXU0uX>l zMkbe9<67ImJ=MAb75BZn3mzrLH>G&JpC{*yMkrywA@AV=tD0nCUuGgcS(7KE5#!UM zl|}W+ty@VrcymiwWYbLx5>L7ypv=NX`Cv@IaY17%jv4{8PXL3#m6oAF2PTE+n5iUQ zNa|?-CH3_+?TMN)qj-;$Y;<@DwX%jPWA}`TaWtBy*jcLl!QDN7%`5vC%nsCuYW??T zI-jjGTETW7i+O6k_A0LW8l86fn9mC|R%f_u2SPYEOV=TJFoNc*m)Q-y^OcdBKTR~Z zxon_Z`@KlljWNmY_&NkoVo4qxz~2O)o00@N1aYq@EDotJurOM*>PPo+eYsWo*;QY2 z$u(*o^nGniQof}6)1c(O@c&= zA(8)3M%13eiE=`&U=v$Mqeo35w?m(Yx`I0L-I5kG_AXUOfjd+2!*&{sy%esY(!^9E zo#)DFTnuY*K^Yt%#l}BlDj6N?&P(r{;Iu#H^OwNmV^`Oe?{Ti(W&}w7DHk&KhgcML zo`gG%YvS>iC1-X{WJs!W--|;OgC7mva!@gL++Jt+RszkzS5rL&6kf zm+||RnqWdF0<%9IKgURPxDOwPv-_*w4}vC)+e*Bg1%h=9q+>;CXW`emJ-!{>D$go) z7T}VKq4DaNa5~cog~mz+8+HnWcp{Xnjzc_lZBFeuK$mn}>4QRM-4>JIxz9`dteTF%f?{NaLeKO3qUv!)m^UD5ch@=Qj?wo?ks4o6nd@KxF3yyH@U+7TX9;AQULz9FoWVDx{>7;MJ=>;E~?IKnE?1GZ0)_ zI!t5ESZtHfC_?JsK&v*Hi*f@om0I-FoO4;`%K5H8Yd$~lDhh`OQ`b{RzE5IpJdq_k zhW$%uLG3 zNIF?dqbVt}9^=zWLG8`?UdE{zk^pgar=|pbu?`DTkB^GVJ+iXKPH8wlkI-Q-L@w`D z0tjI+G+9F<2Gr z`|gHU#N_?5xSID%OPmr-_9}hD()jn}Gh7f<*#NX9JmtN!&EVSEC~XYK4!${rxjt$9 zeShYSNXYJT4MzxBI0~ESpetpcL3Ud>p$+|dz=j%zok%E4m8Dl2Dr;p7cQ8Xz=*e}O z(Pzug11p?wabU;3W!Sh9<;A=~6sw!!F)%H5%K;^xOCC(BfOonT4>hvX+sa-zh6tB! zB*8l!vC}^5C}#E`685*(YnAolV1vd5MZ>1B_B~u;R0W?I0cJ$k+t;HKJt#t=P>+)& z^(U1S79nWqYCw79H`ZUjSoU4bS!^T74ny}ldL7Otqlo6rO}Ds4f|X z7n4nk_1fqG2ZoX^3jkKO{V{AJ6Wn|7!K83o;SCv4nB!8wXFPN=W9q^KIkap%_=Aw& zsIc3}P>=EPvM)ErJWxSnmp{%lqkVPdnDbm-QbR4VxSCsu!P3UW30ULI8r|2Wtl3u*^5V@la)_rmBoO@{T%cSw}WWTBkopk(*RZSw;hV#Oe zl&dr(rgFn*Y@1&p2}bLNX^78^l82{%ZWTbG#t9W!ew}g0X&O-F?ut;jKvr;DOw!1FK## zr~nhE#Cew@JR4J^7&9P{#kgWbpE7`TVLsw9)?jU8@mHT>gH4{?)Oi^94Yj7j+mVg^Q_A}MtCkbtH0*av}uQaS*75-m_quQsOb+2J>qPrId1>#$}a6k zHdywpG~JEovqCy=V`exfO{=YxB*z97>o9Y6r@LF>dFPL3LVEnV0#9y7)99<(%*ad9 zLDmknWFroa(siVc0mpyV+g}e)uN#a|`0r4ad=EM%mMclXK#u*M)jq3aIQjDKbkQ5}S+DkeS+%*mIO; z0VAg=W;f0_2OBoI9_w%2*xXvL+**@Nh6k@JD!ebYMOQKD%ZeY}sSCrCKkXm7ZFQh9 zONgsg1uTsG(qMF$7q6yi>;EA2$`CA<1zG4F`NAuE{D5|A1C^cjh8~cEg5NcKb$q+js|Vm*d9%vsStj7&2qOl znb3YYYjM%5agi#iQ+HOXxKvvlm4Q>m%Vl?w*Fl$x^O+tCmXMT6yANUdy2tUbWa#A-1w9F&C{;2rC1d=4>f=M6%(yPWL+i^(n) zay*k7F_QE^ySRpZ_p!AA0P>)hxRkh5m9$h9maO_D6u+fq%QtKc6c^g~?oFI%j4=nE zZ;LFHWR_Ls&^|}%q^uP$Lr_AJ)(m9_zA(-dl8J!*HG66~?JL@*uFQfLDZa_me@~u| zkwkoBeDZ4>U*(HBa%M|i-izkv!NxtywEWv8GqZkT7#(MSTJD__j zK+)0nm5BI$Z54Rn0tM}(vR>&YyEpk>$ZMh%6AX{NeFbEq5kY^FmyxZb{tG8&{y#*J zbx?(!yVKwEj1laf1il+w8<|~!ROno*Z9-2Z_J%)UDpFx0r@hy*Ex1Ioc=3uPu+FQx z>#I<*5E8!YS>EA}g{~7Eg4Vqfyex59zTlTG`TJ~i(`e1v+;f55llI>-ZKF#+@VqP( z{YB3b3f!P$I5scnvs(cs4!_J9s)8z)6SQf6r8Z8~dV}XDpvRHUJgkqqBUBr~QoKcI zIBS8)Pr1fLqoWDjJ-p;C;;P7vzrQQic|?2cmKYfFY%!ywlYRXu&RQu;C5ux&i?d3f zyXS2#I|1p7xwO;AN7E)rL`$qQJslp{5Pp*A3aO9$*2Sk5lu(kXvUD1GeL%yX*O;;~SQ>z1X2$F8kyoxH=K! z5s>>=t(+;ux8i5qF|bU`bKi`aDE9GOom2#MY%7B)s*GgC?3DB0@#XG6A`f8Y7_wu% ziiXi068`g(m$R-y*=E0N>=DH?n4udwOf+Q!7MzI1)TZi6Q_ zSA={#3VKXIEQqI)p!I$r67ot8|F`mJfSplOH%V&(5~Sf8p8x!VJ6L8&Np1SmM>`i+ zO5RtR#Xjca+bx@JJ9u$vlgq_m0WmRo@TQFAaL5g(UyBGU#LbVd0LE53O~hr37=5sE zTa5Vof{i@AueLE&td1v0Wn$T+YlTmS<8r`mk=l{7Op&w2*CVt~dQZVdjW&R~sqP^} z5UbMDR6eCCM^jZ?pE`H+Do%5OR^+qEe6Ot(O=csr1ptzluYp0T^TO}{B?54&%42)X zaXwjV)y%6z=qmR0XwGVja3r4ER#f)43*?3Q_3K1@xiFe~p=89o9D)m)=V zY~W{9MMPY8*s>$L?lAt<#Dt})r%z3c@^MnFQ>47!<rfOv+)~Htf zXg7fmvB&#F(W&g27gr388>Mg-^MI8u7*0fLE8xRFPELdfm&F17sGx3-@0kkk$5>Uf zcUNvHMaEiVG&xh3jaEB83n2rBm1N%-6T zCSay`vy{;9;W$CZ7IDLPD156Z_$I@NdKOreue67v@K8zL^;8T^L`xsH$&RMQVt9cvSh=NTJMX5&w*3x3_qZvE{Wm#&}BXJcfy0-T$n1jMP3C@pUR4 zdLr`nl|7}8e<}YzvirY?|8fJP5XQz{?_D3H$Q=0;RArOjuR>b1+!F_cO;%XQt?P0@ zwJwU}75eD;A_Fjo@|};v93S6~@Q%t?ulH{_8NjcB;Vq{?L9Wdr^Jm9>v+lWfnccW? zkF(|rxTc3(L0zL;Gzjohl<7aMajPXEcDv&-V%>41Kg(e8AbBym?gq$dGwtJd&E=AK zlO32f^!_^fLvy)4URNsZMeKuH!!}!I*7d#l=8wzA2X>buj6BNM!*3+sxp|WY`e9gg z{h2;kN9{;MxVU!RY5w<)zeqZ?D6e7$te(lft2mt z;?!VZ@jvwR%W^-rEJx1tW`)X#ySzyr0Tz4`%$XTJk>!$64*GAs!3p#c0nW zh4%(y&-3=s$uy{aK1$xrPEE;_`uh9b6(3R5x{8R9q-foXX4)(`q$UpCl1*v%tq-m* z`IAl3oj}{mbEy)P&oRzw9KNgTH)T&vVSPN)orU={n&kW_7j)~&YX60)FdO^HY3P;z zBTz9ub$Ul<>Pm!)L;7(v|9hoIyBcWqeP^VL@02IWvHK#Cgx>hHWidRkx}=AEtm|ah zI$%xQPn7?OvYd=X@*B}7{C!hcykDq6izbN2pf^@EoN)5>>>Mb8#%T69pewuJIv-bD zWTc9YVysr$wl#7rn1c~w4PS%w+!VwrP#0E|WUx$1R;Z*?opY#l{o|sj+dXrgM1|-> zZ|K(9xQ*a{o=*R3zB~$snBWQ&12=YElmt3>pO^=jQ7m?f4JO`#?sV6T_WfFDI~d`q znZ6}MkC=Ej5j}LR>l#D)WG@UslCO&hsT${?e#V?S1BUD^i?>OQu)y&1-{zHa#56p! zldaVueQ{n3?E0qyqMR}%rg3}G?$7v`zBz#fiR{Yve~jNyNW7Z{w8a{yOMl2oac@!# zIu9=#yT*ou&AYfg$iF2Y=lar1fS*vc;C8S?_VIMG`n=!RW#bHb0L>lvFzXbTqB~>% zW$Kn9`FP#?>Y~}yr+OhljVkYRJB6X3#&!J+?%g3$p(pog5^5D^}Yt84TW^@X#{ZHa6ixtjaFg*H2 zrMz$}?KJ9;!tbFYztam*6bJd^QbT`*ATjPr#mJ07=EuH2TA-sa)6fX^&N8E5p5?Xx z{)S|&n;+N!r8T5P7`tKf?e#hgLp0-CW4+aP$yWZa*OIp`Cw#(`FsGuOBV9O1Lk#?W zKB>1SKB!Mx3M33T4H(IeX41J5F=+`T!r7&Pv7zDe;@QR7WY}Sya(!qDx0=n#eN0N55rSkK6lov%!!?Y6A zH3z64YlQGRVEBg);2Mmoq%$hOo~ZJ zEJJr1Eo}|0eYoYPXD=zWxEk47;93X=cTTKmWBnNh(XTVba)7%A{hY~msP9q068d8= z$?$q>LVzMO;I=4qL3o3q?(d8!JVgU8f#X4_8Lhz_XMfP-R~r+xe4|#d=%IoHH~L%^ zn=&1&>J(H6sOThdOjW{YkW%Ao45~8Oo*NHwtc)R^!Il5k-A(@cp(l6*!XuaT0Sw4KcGWe*@p z!;vZ+2q)T&LiL>9s381Zk1DV)r{(tjN$4%QQsDK~k1XcS?8R&?J|rqf1E=oShS&ct zO+q?OUq@P4wTUadjHIn82Y2m7Azz)TLWqYg`zYjDUa(xy%AFuwV(#ousgD zYcn5HaBRe;6=+R}K!g5J<-tQ+B^*;PG`P4rlPDGjvvbO$uGeJ>lz*LlzDc-Pb1LU4R?Mh~;;Mt#G;P zr$+k{k3BC;!TLa^R*Cg-?GW;)FHamEwxPBBi6XiLBjIkK?L@=teD4IPa#XcR)6x=7 zNf*(e>lRw!gxe|zu>2%8{Y7NAuWxe+@V$H9aWz5upO)}Ug0@b#ne}@2wieL#^Xo~u zsD$Rn5XWpJ#6w^5fFXOXS*~}EGrZ)8f_4%@m_7Z40fc-sXgcev8T5>wl;~3Lg6D8X zVlw@MRY|^yi2lX<N)Im9X;(teY$4iorVA*iZ|d%^i&}Na=snmp-r)93Q@j-vQUwd$Uaj zo=t#!Z9LephS_W9b!LV0eT=Ii%{90LKp~+^OF94pD}QPXA@4ldc+3%YtFw7Gk-Rq? zeW4${01y(XGEq{)9lsRmS8Y`J<3wl5jobR&OBR+y{%K|%OH81{#haBBg(*!kD?vkm z?72Y~=H`qSwPlH%dmXQGEMKrGAkske%}BhgOGjUEM2Aq)u#dr*&y8o$z8#W}Hh7nJ zNGXnq*_zDFO_@=VvNpDbTV8>L%&ahIDK31+Aj=Pc7#(jvG@}`}q7gr~_;ce$+L0W; z1L#}RnHb1k|h3qK3msL8@wqjOfp`EG7PP-V$VZ{Kf@!`E=V zPgX;Z8n~yYYd2`#wzqBn2;XL-}x@j;XD39u0+P!=nizp_F9_^P%Sj>wkv_{A^5&|fFNu4s%d>+XBe-w6oaGlh+=>NO);}1sIT;ckjNId7$p8zPz^6GNc IGG@X51%Gc=9RL6T literal 0 HcmV?d00001 diff --git a/requirements/requirements-api.txt b/requirements/requirements-api.txt index 293521a79..703fef86b 100644 --- a/requirements/requirements-api.txt +++ b/requirements/requirements-api.txt @@ -32,6 +32,10 @@ click==8.3.1 # rich-toolkit # typer # uvicorn +colorama==0.4.6 + # via + # click + # uvicorn cryptography==46.0.6 # via # authlib @@ -65,6 +69,8 @@ fastar==0.9.0 # via fastapi-cloud-cli fief-client==0.20.0 # via carbonserver (carbonserver/pyproject.toml) +greenlet==3.3.2 + # via sqlalchemy h11==0.16.0 # via # httpcore @@ -208,8 +214,6 @@ uvicorn==0.38.0 # fastapi # fastapi-cli # fastapi-cloud-cli -uvloop==0.22.1 - # via uvicorn watchfiles==1.1.1 # via uvicorn websockets==15.0.1 From 8ca4b86086ec116e5510e4a5d6ee8a6d39223215 Mon Sep 17 00:00:00 2001 From: j-rebell Date: Tue, 7 Apr 2026 09:33:31 -0400 Subject: [PATCH 08/29] Made a test to ensure changes to hardware.py produce expected output. --- tests/test_hardware.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_hardware.py diff --git a/tests/test_hardware.py b/tests/test_hardware.py new file mode 100644 index 000000000..4dc7c6b0c --- /dev/null +++ b/tests/test_hardware.py @@ -0,0 +1,11 @@ +from codecarbon.external import hardware + +def test_output(): + temp = hardware.CPU.get_cpu_temperature() + assert type(temp) == float + temp_in_expected_range = False + for x in range(15000): + x_to_float = float(x/100) + if round(temp, 2) == x_to_float : + temp_in_expected_range = True + assert temp_in_expected_range == True From 0507f02fa12556b0c1c6f8929fe50373accfd6dd Mon Sep 17 00:00:00 2001 From: j-rebell Date: Tue, 7 Apr 2026 09:46:26 -0400 Subject: [PATCH 09/29] Made a test to ensure changes to hardware.py produce expected output. --- tests/test_hardware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_hardware.py b/tests/test_hardware.py index 4dc7c6b0c..ae88cd242 100644 --- a/tests/test_hardware.py +++ b/tests/test_hardware.py @@ -1,11 +1,11 @@ from codecarbon.external import hardware + def test_output(): temp = hardware.CPU.get_cpu_temperature() - assert type(temp) == float temp_in_expected_range = False for x in range(15000): - x_to_float = float(x/100) + x_to_float = float(x / 100) if round(temp, 2) == x_to_float : temp_in_expected_range = True - assert temp_in_expected_range == True + assert temp_in_expected_range From d9fc3df1e4a8686bc9b2c6b03bbe73689da7f215 Mon Sep 17 00:00:00 2001 From: JargusB Date: Wed, 8 Apr 2026 22:20:27 -0400 Subject: [PATCH 10/29] removed test_temp.yml and test_temp.py, wew used for debugging purposes --- .github/workflows/test_temp.yml | 58 --------------------------------- codecarbon/test_temp.py | 39 ---------------------- 2 files changed, 97 deletions(-) delete mode 100644 .github/workflows/test_temp.yml delete mode 100644 codecarbon/test_temp.py diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml deleted file mode 100644 index b9fa9a409..000000000 --- a/.github/workflows/test_temp.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Test Temperature Tracking - -on: - push: - branches: [master] - workflow_dispatch: - -jobs: - test-temperature: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -e . - pip install pandas - - - name: Check sensors available - run: | - sudo apt-get install -y lm-sensors - python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" - - - name: Run temperature test - run: | - python3 -c " - import time - from codecarbon import EmissionsTracker - - tracker = EmissionsTracker( - project_name='temperature_test', - measure_power_secs=15, - save_to_file=True, - output_file='emissions_temp_test.csv', - log_level='debug' - ) - - tracker.start() - total = sum(range(10_000_000)) - time.sleep(30) - emissions = tracker.stop() - - print(f'Emissions: {emissions:.6f} kg CO2') - print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') - print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') - - import pandas as pd - df = pd.read_csv('emissions_temp_test.csv') - print('CSV columns:', df.columns.tolist()) - print('Temperature values:') - print(df[['cpu_temperature', 'gpu_temperature']]) - " diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py deleted file mode 100644 index 38377f555..000000000 --- a/codecarbon/test_temp.py +++ /dev/null @@ -1,39 +0,0 @@ -# test_temp.py -import time - -import pandas as pd - -from codecarbon import EmissionsTracker - -tracker = EmissionsTracker( - project_name="temperature_test", - measure_power_secs=15, - save_to_file=True, - output_file="emissions_temp_test.csv", - log_level="debug", -) - -tracker.start() - -# simulate some work -print("Running workload...") -total = 0 -for i in range(10_000_000): - total += i - -time.sleep(30) # give monitor_power time to collect samples - -emissions = tracker.stop() - -# check results -print("\n--- Results ---") -print("Emissions: {emissions:.6f} kg CO2") -print("CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") -print("GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") - -# verify CSV -df = pd.read_csv("emissions_temp_test.csv") -print("\n--- CSV columns ---") -print(df.columns.tolist()) -print("\n--- Temperature values in CSV ---") -print(df[["cpu_temperature", "gpu_temperature"]]) From 5b5fad0cec2c80fb1018c162a991c74671fb61c7 Mon Sep 17 00:00:00 2001 From: j1rebell Date: Thu, 9 Apr 2026 10:00:37 -0400 Subject: [PATCH 11/29] Changed testing to test for psutil returning a value in test_cpu.py. intel_power_gadget is already tested to return temperature in the same file so there was no need to add anything additional. --- tests/test_cpu.py | 7 +++++++ tests/test_hardware.py | 11 ----------- 2 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 tests/test_hardware.py diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 1e1308812..eadd60bf6 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -5,6 +5,7 @@ import unittest from unittest import mock +import psutil import pytest from codecarbon.core.config import normalize_gpu_ids @@ -66,6 +67,11 @@ def test_is_psutil_available_without_nice(self, mock_cpu_times): def test_is_psutil_not_available_on_exception(self, mock_cpu_times): self.assertFalse(is_psutil_available()) + @mock.patch("psutil.sensors_temperatures") + def psutil_returns_expected_temperature(self, mock_cpu_times): + mock_temp = mock.Mock() + mock_temp.return_value = {"coretemp" : 50, "k10temp" : 50, "cpu_thermal" : 50} + self.assertEqual(psutil.sensors_temperatures(), 50) class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): @@ -850,3 +856,4 @@ def test_count_physical_cpus_linux(self): side_effect=subprocess.CalledProcessError(1, "lscpu"), ): assert count_physical_cpus() == 1 + diff --git a/tests/test_hardware.py b/tests/test_hardware.py deleted file mode 100644 index ae88cd242..000000000 --- a/tests/test_hardware.py +++ /dev/null @@ -1,11 +0,0 @@ -from codecarbon.external import hardware - - -def test_output(): - temp = hardware.CPU.get_cpu_temperature() - temp_in_expected_range = False - for x in range(15000): - x_to_float = float(x / 100) - if round(temp, 2) == x_to_float : - temp_in_expected_range = True - assert temp_in_expected_range From d6eedfc71d5393d8da787832e5a7e5ac6e7a4567 Mon Sep 17 00:00:00 2001 From: j1rebell Date: Thu, 9 Apr 2026 10:16:06 -0400 Subject: [PATCH 12/29] Changed testing to test for psutil returning a value in test_cpu.py. intel_power_gadget is already tested to return temperature in the same file so there was no need to add anything additional. --- tests/test_cpu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index eadd60bf6..c18bcae2a 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -70,9 +70,10 @@ def test_is_psutil_not_available_on_exception(self, mock_cpu_times): @mock.patch("psutil.sensors_temperatures") def psutil_returns_expected_temperature(self, mock_cpu_times): mock_temp = mock.Mock() - mock_temp.return_value = {"coretemp" : 50, "k10temp" : 50, "cpu_thermal" : 50} + mock_temp.return_value = {"coretemp": 50, "k10temp": 50, "cpu_thermal": 50} self.assertEqual(psutil.sensors_temperatures(), 50) + class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): with tempfile.TemporaryDirectory() as parent: @@ -856,4 +857,3 @@ def test_count_physical_cpus_linux(self): side_effect=subprocess.CalledProcessError(1, "lscpu"), ): assert count_physical_cpus() == 1 - From 7a72419fa8fc9eacea2e770fea16e320c10ec870 Mon Sep 17 00:00:00 2001 From: Pat3690 <87405996+Pat3690@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:47:21 -0400 Subject: [PATCH 13/29] Updated Documentation, can be found in /docs/Contributions --- docs/Contributions/cputemp.md | 63 ++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/Contributions/cputemp.md b/docs/Contributions/cputemp.md index 8a4574e9f..c656cb258 100644 --- a/docs/Contributions/cputemp.md +++ b/docs/Contributions/cputemp.md @@ -39,8 +39,69 @@ def get_cpu_temperature(self) -> float: logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") return 0.0 ``` +### Added Workflow + +```python +name: Test Temperature Tracking + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + test-temperature: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -e . + pip install pandas + + - name: Check sensors available + run: | + sudo apt-get install -y lm-sensors + python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" + + - name: Run temperature test + run: | + python3 -c " + import time + from codecarbon import EmissionsTracker + + tracker = EmissionsTracker( + project_name='temperature_test', + measure_power_secs=15, + save_to_file=True, + output_file='emissions_temp_test.csv', + log_level='debug' + ) + + tracker.start() + total = sum(range(10_000_000)) + time.sleep(30) + emissions = tracker.stop() + + print(f'Emissions: {emissions:.6f} kg CO2') + print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') + print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') + + import pandas as pd + df = pd.read_csv('emissions_temp_test.csv') + print('CSV columns:', df.columns.tolist()) + print('Temperature values:') + print(df[['cpu_temperature', 'gpu_temperature']]) + " +``` Allowed for CodeCarbon to track it and input it in to the CSV data set, shown in terminal below ![](../images/CpuTemp.png){.align-center width="700px" height="400px"} -Make sure to run the 'test_temp.py' file From 00477877eff81981e1c530590f37c214db2e8d59 Mon Sep 17 00:00:00 2001 From: JargusB Date: Fri, 17 Apr 2026 21:09:37 -0400 Subject: [PATCH 14/29] added accelerator.py file for added inferentia chip support, open to further implementations of ai accelerator chips --- codecarbon/core/ai_accelerator.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 codecarbon/core/ai_accelerator.py diff --git a/codecarbon/core/ai_accelerator.py b/codecarbon/core/ai_accelerator.py new file mode 100644 index 000000000..e69de29bb From 76d59e60715b7e434631e5a61b0b70d6f12745c9 Mon Sep 17 00:00:00 2001 From: JargusB Date: Sun, 5 Apr 2026 18:00:23 -0400 Subject: [PATCH 15/29] added a function to hardware.py for cpu temp tracking, edited emissions tracker to handle gpu and cpu temps, edited emissions_data.py to export this data to the csv on output --- codecarbon/emissions_tracker.py | 39 +++++++++++++++++---- codecarbon/external/hardware.py | 37 +++++++++++++++++++ codecarbon/output_methods/emissions_data.py | 4 +++ codecarbon/test_temp.py | 37 +++++++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 codecarbon/test_temp.py diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 862eba2b4..f9ce5ee6d 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -368,6 +368,8 @@ def __init__( self._gpu_utilization_history: List[float] = [] self._ram_utilization_history: List[float] = [] self._ram_used_history: List[float] = [] + self._cpu_temperature_history: List[float] = [] + self._gpu_temperature_history: List[float] = [] self._total_cpu_energy: Energy = Energy.from_energy(kWh=0) self._total_gpu_energy: Energy = Energy.from_energy(kWh=0) self._total_ram_energy: Energy = Energy.from_energy(kWh=0) @@ -548,6 +550,8 @@ def start(self) -> None: self._ram_utilization_history.clear() self._ram_used_history.clear() self._gpu_utilization_history.clear() + self._cpu_temperature_history.clear() + self._gpu_temperature_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -598,6 +602,8 @@ def start_task(self, task_name=None) -> None: self._ram_utilization_history.clear() self._ram_used_history.clear() self._gpu_utilization_history.clear() + self._cpu_temperature_history.clear() + self._gpu_temperature_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -922,6 +928,16 @@ def _prepare_emissions_data(self) -> EmissionsData: tracking_mode=self._conf.get("tracking_mode"), pue=self._pue, wue=self._wue, + cpu_temperature=( + sum(self._cpu_temperature_history) / len(self._cpu_temperature_history) + if self._cpu_temperature_history + else 0.0 + ), + gpu_temperature=( + sum(self._gpu_temperature_history) / len(self._gpu_temperature_history) + if self._gpu_temperature_history + else 0.0 + ), ) logger.debug(total_emissions) return total_emissions @@ -973,6 +989,10 @@ def _monitor_power(self) -> None: self._ram_utilization_history.append(psutil.virtual_memory().percent) self._ram_used_history.append(psutil.virtual_memory().used / (1024**3)) + for hardware in self._hardware: + if isinstance(hardware, CPU): + self._cpu_temperature_history.append(hardware.get_cpu_temperature()) + # Collect GPU utilization metrics for hardware in self._hardware: if isinstance(hardware, GPU): @@ -980,13 +1000,18 @@ def _monitor_power(self) -> None: gpu_details = hardware.devices.get_gpu_details() for gpu_index, gpu_detail in enumerate(gpu_details): resolved_gpu_index = gpu_detail.get("gpu_index", gpu_index) - if ( - resolved_gpu_index in gpu_ids_to_monitor - and "gpu_utilization" in gpu_detail - ): - self._gpu_utilization_history.append( - gpu_detail["gpu_utilization"] - ) + if resolved_gpu_index in gpu_ids_to_monitor: + + if "gpu_utilization" in gpu_detail: + self._gpu_utilization_history.append( + gpu_detail["gpu_utilization"] + ) + + if "temperature" in gpu_detail: + self._gpu_temperature_history.append( + gpu_detail["temperature"] + ) + def _do_measurements(self) -> None: for hardware in self._hardware: diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 8ac4de8f8..3aad344c2 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -409,9 +409,46 @@ def monitor_power(self): cpu_power = self._get_power_from_cpus() self._power_history.append(cpu_power) + + def get_cpu_temperature(self) -> float: + """ + Get average CPU temperature in Celsius. + Supported on Linux (Intel + AMD) and Windows Intel via Power Gadget. + Returns 0.0 if temperature cannot be read on the current platform. + """ + try: + if self._mode == "intel_power_gadget": + all_cpu_details = self._intel_interface.get_cpu_details() + for metric, value in all_cpu_details.items(): + if re.match(r"^CPU Temperature", metric): + return float(value) + return 0.0 + + elif self._mode in ["intel_rapl", MODE_CPU_LOAD, "constant"]: + temps = psutil.sensors_temperatures() + if not temps: + logger.debug( + "get_cpu_temperature: psutil.sensors_temperatures() " + "returned no data on this platform" + ) + return 0.0 + for key in ["coretemp", "k10temp", "cpu_thermal"]: + if key in temps: + readings = temps[key] + avg = sum(r.current for r in readings) / len(readings) + logger.debug( + f"get_cpu_temperature: {key} avg = {avg:.1f}°C" + ) + return avg + return 0.0 + + except Exception as e: + logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") + return 0.0 def get_model(self): return self._model + @classmethod def from_utils( cls, diff --git a/codecarbon/output_methods/emissions_data.py b/codecarbon/output_methods/emissions_data.py index 17544aa51..b086f3eca 100644 --- a/codecarbon/output_methods/emissions_data.py +++ b/codecarbon/output_methods/emissions_data.py @@ -47,6 +47,8 @@ class EmissionsData: on_cloud: str = "N" pue: float = 1 wue: float = 0 + cpu_temperature: float = 0.0 # ADD + gpu_temperature: float = 0.0 # ADD @property def values(self) -> OrderedDict: @@ -110,6 +112,8 @@ class TaskEmissionsData: ram_utilization_percent: float = 0.0 ram_used_gb: float = 0.0 on_cloud: str = "N" + cpu_temperature: float = 0.0 + gpu_temperature: float = 0.0 @property def values(self) -> OrderedDict: diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py new file mode 100644 index 000000000..db5bac723 --- /dev/null +++ b/codecarbon/test_temp.py @@ -0,0 +1,37 @@ +# test_temp.py +import time +import pandas as pd +from codecarbon import EmissionsTracker + +tracker = EmissionsTracker( + project_name="temperature_test", + measure_power_secs=15, + save_to_file=True, + output_file="emissions_temp_test.csv", + log_level="debug" +) + +tracker.start() + +# simulate some work +print("Running workload...") +total = 0 +for i in range(10_000_000): + total += i + +time.sleep(30) # give monitor_power time to collect samples + +emissions = tracker.stop() + +# check results +print(f"\n--- Results ---") +print(f"Emissions: {emissions:.6f} kg CO2") +print(f"CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") +print(f"GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") + +# verify CSV +df = pd.read_csv("emissions_temp_test.csv") +print(f"\n--- CSV columns ---") +print(df.columns.tolist()) +print(f"\n--- Temperature values in CSV ---") +print(df[["cpu_temperature", "gpu_temperature"]]) From 26819dea83d0693e4f16beaaac332e2ddc309955 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:33:24 -0400 Subject: [PATCH 16/29] added temp tracking test workflow --- .github/workflows/test_temp.yml | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/test_temp.yml diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml new file mode 100644 index 000000000..320e78431 --- /dev/null +++ b/.github/workflows/test_temp.yml @@ -0,0 +1,58 @@ +name: Test Temperature Tracking + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + test-temperature: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -e . + pip install pandas + + - name: Check sensors available + run: | + sudo apt-get install -y lm-sensors + python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" + + - name: Run temperature test + run: | + python3 -c " + import time + from codecarbon import EmissionsTracker + + tracker = EmissionsTracker( + project_name='temperature_test', + measure_power_secs=15, + save_to_file=True, + output_file='emissions_temp_test.csv', + log_level='debug' + ) + + tracker.start() + total = sum(range(10_000_000)) + time.sleep(30) + emissions = tracker.stop() + + print(f'Emissions: {emissions:.6f} kg CO2') + print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') + print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') + + import pandas as pd + df = pd.read_csv('emissions_temp_test.csv') + print('CSV columns:', df.columns.tolist()) + print('Temperature values:') + print(df[['cpu_temperature', 'gpu_temperature']]) + " From a31391aabb4132473678e53d985e13cea73802e4 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:38:11 -0400 Subject: [PATCH 17/29] Fix flake8 f-string warnings in test file --- codecarbon/test_temp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py index db5bac723..f2ac686d2 100644 --- a/codecarbon/test_temp.py +++ b/codecarbon/test_temp.py @@ -24,14 +24,14 @@ emissions = tracker.stop() # check results -print(f"\n--- Results ---") -print(f"Emissions: {emissions:.6f} kg CO2") -print(f"CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") -print(f"GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") +print("\n--- Results ---") +print("Emissions: {emissions:.6f} kg CO2") +print("CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") +print("GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") # verify CSV df = pd.read_csv("emissions_temp_test.csv") -print(f"\n--- CSV columns ---") +print("\n--- CSV columns ---") print(df.columns.tolist()) -print(f"\n--- Temperature values in CSV ---") +print("\n--- Temperature values in CSV ---") print(df[["cpu_temperature", "gpu_temperature"]]) From 87994fb8e4b170b9804a58dbeabc045251ba5c34 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 13:45:47 -0400 Subject: [PATCH 18/29] Apply black and isort formatting fixes --- codecarbon/emissions_tracker.py | 1 - codecarbon/external/hardware.py | 7 ++----- codecarbon/test_temp.py | 4 +++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index f9ce5ee6d..d2b43c81f 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -1012,7 +1012,6 @@ def _monitor_power(self) -> None: gpu_detail["temperature"] ) - def _do_measurements(self) -> None: for hardware in self._hardware: h_time = time.perf_counter() diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 3aad344c2..fa0ac50cf 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -409,7 +409,6 @@ def monitor_power(self): cpu_power = self._get_power_from_cpus() self._power_history.append(cpu_power) - def get_cpu_temperature(self) -> float: """ Get average CPU temperature in Celsius. @@ -436,19 +435,17 @@ def get_cpu_temperature(self) -> float: if key in temps: readings = temps[key] avg = sum(r.current for r in readings) / len(readings) - logger.debug( - f"get_cpu_temperature: {key} avg = {avg:.1f}°C" - ) + logger.debug(f"get_cpu_temperature: {key} avg = {avg:.1f}°C") return avg return 0.0 except Exception as e: logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") return 0.0 + def get_model(self): return self._model - @classmethod def from_utils( cls, diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py index f2ac686d2..38377f555 100644 --- a/codecarbon/test_temp.py +++ b/codecarbon/test_temp.py @@ -1,6 +1,8 @@ # test_temp.py import time + import pandas as pd + from codecarbon import EmissionsTracker tracker = EmissionsTracker( @@ -8,7 +10,7 @@ measure_power_secs=15, save_to_file=True, output_file="emissions_temp_test.csv", - log_level="debug" + log_level="debug", ) tracker.start() From 694e24d41233c102eb1aa4a3968f307b88a209e2 Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 14:00:50 -0400 Subject: [PATCH 19/29] Add cpu_temperature and gpu_temperature to test data CSV --- tests/test_data/emissions_valid_headers.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_data/emissions_valid_headers.csv b/tests/test_data/emissions_valid_headers.csv index b7493c902..aaf9ad182 100644 --- a/tests/test_data/emissions_valid_headers.csv +++ b/tests/test_data/emissions_valid_headers.csv @@ -1,2 +1,2 @@ -timestamp,project_name,run_id,experiment_id,duration,emissions,emissions_rate,cpu_power,gpu_power,ram_power,cpu_energy,gpu_energy,ram_energy,energy_consumed,water_consumed,country_name,country_iso_code,region,cloud_provider,cloud_region,os,python_version,codecarbon_version,cpu_count,cpu_model,gpu_count,gpu_model,longitude,latitude,ram_total_size,tracking_mode,cpu_utilization_percent,gpu_utilization_percent,ram_utilization_percent,ram_used_gb,on_cloud,pue,wue +timestamp,project_name,run_id,experiment_id,duration,emissions,emissions_rate,cpu_power,gpu_power,ram_power,cpu_energy,gpu_energy,ram_energy,energy_consumed,water_consumed,country_name,country_iso_code,region,cloud_provider,cloud_region,os,python_version,codecarbon_version,cpu_count,cpu_model,gpu_count,gpu_model,longitude,latitude,ram_total_size,tracking_mode,cpu_utilization_percent,gpu_utilization_percent,ram_utilization_percent,ram_used_gb,on_cloud,pue,wue,cpu_temperature,gpu_temperature 2021-09-23T15:04:51,codecarbon,0a578547-1d6b-4e2f-be0c-7ad10f2f7c97,test,161.20380687713623,0.0004490989249167,0.0027859076880178,0.269999999999999,0.0,12.884901888000002,0.0,0,0.00057442898176,0.00057442898176,0.1,Morocco,MAR,casablanca-settat,,,macOS-10.15.7-x86_64-i386-64bit,3.8.0,2.1.3,12,Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz,,,-7.9084,33.5932,,machine,0.0,0.0,0.0,0.0,N,1.0,0.0 From 28f1829eba87faa60ce43cc97ac8e0f95afdfb0b Mon Sep 17 00:00:00 2001 From: JargusB Date: Mon, 6 Apr 2026 14:11:28 -0400 Subject: [PATCH 20/29] changed test_temp workflow naming from main to master --- .github/workflows/test_temp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml index 320e78431..b9fa9a409 100644 --- a/.github/workflows/test_temp.yml +++ b/.github/workflows/test_temp.yml @@ -2,7 +2,7 @@ name: Test Temperature Tracking on: push: - branches: [ main ] + branches: [master] workflow_dispatch: jobs: From 75d0852a826398f2bc73e241ed28c4f5ce25c79f Mon Sep 17 00:00:00 2001 From: j-rebell Date: Tue, 7 Apr 2026 09:33:31 -0400 Subject: [PATCH 21/29] Made a test to ensure changes to hardware.py produce expected output. --- tests/test_hardware.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_hardware.py diff --git a/tests/test_hardware.py b/tests/test_hardware.py new file mode 100644 index 000000000..4dc7c6b0c --- /dev/null +++ b/tests/test_hardware.py @@ -0,0 +1,11 @@ +from codecarbon.external import hardware + +def test_output(): + temp = hardware.CPU.get_cpu_temperature() + assert type(temp) == float + temp_in_expected_range = False + for x in range(15000): + x_to_float = float(x/100) + if round(temp, 2) == x_to_float : + temp_in_expected_range = True + assert temp_in_expected_range == True From 5d2149365d20e66df90e843591953a99c2576b0e Mon Sep 17 00:00:00 2001 From: Pat3690 <87405996+Pat3690@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:06:22 -0400 Subject: [PATCH 22/29] Rebase on upstream/master --- docs/Contributions/cputemp.md | 46 ++++++++++++++++++++++++++++++ docs/images/CpuTemp.png | Bin 0 -> 8339 bytes requirements/requirements-api.txt | 8 ++++-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 docs/Contributions/cputemp.md create mode 100644 docs/images/CpuTemp.png diff --git a/docs/Contributions/cputemp.md b/docs/Contributions/cputemp.md new file mode 100644 index 000000000..8a4574e9f --- /dev/null +++ b/docs/Contributions/cputemp.md @@ -0,0 +1,46 @@ +# Contributions + +Added a function in Hardware.py that tracks cpu temps live in Celsius, this covers issue 1008 + +### Added code + +``` python +def get_cpu_temperature(self) -> float: + """ + Get average CPU temperature in Celsius. + Supported on Linux (Intel + AMD) and Windows Intel via Power Gadget. + Returns 0.0 if temperature cannot be read on the current platform. + """ + try: + if self._mode == "intel_power_gadget": + all_cpu_details = self._intel_interface.get_cpu_details() + for metric, value in all_cpu_details.items(): + if re.match(r"^CPU Temperature", metric): + return float(value) + return 0.0 + + elif self._mode in ["intel_rapl", MODE_CPU_LOAD, "constant"]: + temps = psutil.sensors_temperatures() + if not temps: + logger.debug( + "get_cpu_temperature: psutil.sensors_temperatures() " + "returned no data on this platform" + ) + return 0.0 + for key in ["coretemp", "k10temp", "cpu_thermal"]: + if key in temps: + readings = temps[key] + avg = sum(r.current for r in readings) / len(readings) + logger.debug(f"get_cpu_temperature: {key} avg = {avg:.1f}°C") + return avg + return 0.0 + + except Exception as e: + logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") + return 0.0 +``` + +Allowed for CodeCarbon to track it and input it in to the CSV data set, shown in terminal below +![](../images/CpuTemp.png){.align-center width="700px" height="400px"} + +Make sure to run the 'test_temp.py' file diff --git a/docs/images/CpuTemp.png b/docs/images/CpuTemp.png new file mode 100644 index 0000000000000000000000000000000000000000..463dc24f745acd40041fa882d4678f2b8a785fff GIT binary patch literal 8339 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 IGG@X51%Gc=9RL6T literal 0 HcmV?d00001 diff --git a/requirements/requirements-api.txt b/requirements/requirements-api.txt index ce02b80ac..545532048 100644 --- a/requirements/requirements-api.txt +++ b/requirements/requirements-api.txt @@ -32,6 +32,10 @@ click==8.3.1 # rich-toolkit # typer # uvicorn +colorama==0.4.6 + # via + # click + # uvicorn cryptography==46.0.7 # via # authlib @@ -65,6 +69,8 @@ fastar==0.9.0 # via fastapi-cloud-cli fief-client==0.20.0 # via carbonserver (carbonserver/pyproject.toml) +greenlet==3.3.2 + # via sqlalchemy h11==0.16.0 # via # httpcore @@ -208,8 +214,6 @@ uvicorn==0.38.0 # fastapi # fastapi-cli # fastapi-cloud-cli -uvloop==0.22.1 - # via uvicorn watchfiles==1.1.1 # via uvicorn websockets==15.0.1 From aba2f39335e755375e5cd5f68efcefb0b251583c Mon Sep 17 00:00:00 2001 From: j-rebell Date: Tue, 7 Apr 2026 09:46:26 -0400 Subject: [PATCH 23/29] Made a test to ensure changes to hardware.py produce expected output. --- tests/test_hardware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_hardware.py b/tests/test_hardware.py index 4dc7c6b0c..ae88cd242 100644 --- a/tests/test_hardware.py +++ b/tests/test_hardware.py @@ -1,11 +1,11 @@ from codecarbon.external import hardware + def test_output(): temp = hardware.CPU.get_cpu_temperature() - assert type(temp) == float temp_in_expected_range = False for x in range(15000): - x_to_float = float(x/100) + x_to_float = float(x / 100) if round(temp, 2) == x_to_float : temp_in_expected_range = True - assert temp_in_expected_range == True + assert temp_in_expected_range From 42561dac7b4af2c46d38823af041c9176896fbce Mon Sep 17 00:00:00 2001 From: JargusB Date: Wed, 8 Apr 2026 22:20:27 -0400 Subject: [PATCH 24/29] removed test_temp.yml and test_temp.py, wew used for debugging purposes --- .github/workflows/test_temp.yml | 58 --------------------------------- codecarbon/test_temp.py | 39 ---------------------- 2 files changed, 97 deletions(-) delete mode 100644 .github/workflows/test_temp.yml delete mode 100644 codecarbon/test_temp.py diff --git a/.github/workflows/test_temp.yml b/.github/workflows/test_temp.yml deleted file mode 100644 index b9fa9a409..000000000 --- a/.github/workflows/test_temp.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Test Temperature Tracking - -on: - push: - branches: [master] - workflow_dispatch: - -jobs: - test-temperature: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -e . - pip install pandas - - - name: Check sensors available - run: | - sudo apt-get install -y lm-sensors - python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" - - - name: Run temperature test - run: | - python3 -c " - import time - from codecarbon import EmissionsTracker - - tracker = EmissionsTracker( - project_name='temperature_test', - measure_power_secs=15, - save_to_file=True, - output_file='emissions_temp_test.csv', - log_level='debug' - ) - - tracker.start() - total = sum(range(10_000_000)) - time.sleep(30) - emissions = tracker.stop() - - print(f'Emissions: {emissions:.6f} kg CO2') - print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') - print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') - - import pandas as pd - df = pd.read_csv('emissions_temp_test.csv') - print('CSV columns:', df.columns.tolist()) - print('Temperature values:') - print(df[['cpu_temperature', 'gpu_temperature']]) - " diff --git a/codecarbon/test_temp.py b/codecarbon/test_temp.py deleted file mode 100644 index 38377f555..000000000 --- a/codecarbon/test_temp.py +++ /dev/null @@ -1,39 +0,0 @@ -# test_temp.py -import time - -import pandas as pd - -from codecarbon import EmissionsTracker - -tracker = EmissionsTracker( - project_name="temperature_test", - measure_power_secs=15, - save_to_file=True, - output_file="emissions_temp_test.csv", - log_level="debug", -) - -tracker.start() - -# simulate some work -print("Running workload...") -total = 0 -for i in range(10_000_000): - total += i - -time.sleep(30) # give monitor_power time to collect samples - -emissions = tracker.stop() - -# check results -print("\n--- Results ---") -print("Emissions: {emissions:.6f} kg CO2") -print("CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}°C") -print("GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}°C") - -# verify CSV -df = pd.read_csv("emissions_temp_test.csv") -print("\n--- CSV columns ---") -print(df.columns.tolist()) -print("\n--- Temperature values in CSV ---") -print(df[["cpu_temperature", "gpu_temperature"]]) From 6b161c8abd5ab2b823ef04d8de1f7c30fe52831f Mon Sep 17 00:00:00 2001 From: j1rebell Date: Thu, 9 Apr 2026 10:00:37 -0400 Subject: [PATCH 25/29] Changed testing to test for psutil returning a value in test_cpu.py. intel_power_gadget is already tested to return temperature in the same file so there was no need to add anything additional. --- tests/test_cpu.py | 7 +++++++ tests/test_hardware.py | 11 ----------- 2 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 tests/test_hardware.py diff --git a/tests/test_cpu.py b/tests/test_cpu.py index 1e1308812..eadd60bf6 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -5,6 +5,7 @@ import unittest from unittest import mock +import psutil import pytest from codecarbon.core.config import normalize_gpu_ids @@ -66,6 +67,11 @@ def test_is_psutil_available_without_nice(self, mock_cpu_times): def test_is_psutil_not_available_on_exception(self, mock_cpu_times): self.assertFalse(is_psutil_available()) + @mock.patch("psutil.sensors_temperatures") + def psutil_returns_expected_temperature(self, mock_cpu_times): + mock_temp = mock.Mock() + mock_temp.return_value = {"coretemp" : 50, "k10temp" : 50, "cpu_thermal" : 50} + self.assertEqual(psutil.sensors_temperatures(), 50) class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): @@ -850,3 +856,4 @@ def test_count_physical_cpus_linux(self): side_effect=subprocess.CalledProcessError(1, "lscpu"), ): assert count_physical_cpus() == 1 + diff --git a/tests/test_hardware.py b/tests/test_hardware.py deleted file mode 100644 index ae88cd242..000000000 --- a/tests/test_hardware.py +++ /dev/null @@ -1,11 +0,0 @@ -from codecarbon.external import hardware - - -def test_output(): - temp = hardware.CPU.get_cpu_temperature() - temp_in_expected_range = False - for x in range(15000): - x_to_float = float(x / 100) - if round(temp, 2) == x_to_float : - temp_in_expected_range = True - assert temp_in_expected_range From 77693625c33a59f3dd6d0235281e92d3875c076e Mon Sep 17 00:00:00 2001 From: j1rebell Date: Thu, 9 Apr 2026 10:16:06 -0400 Subject: [PATCH 26/29] Changed testing to test for psutil returning a value in test_cpu.py. intel_power_gadget is already tested to return temperature in the same file so there was no need to add anything additional. --- tests/test_cpu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cpu.py b/tests/test_cpu.py index eadd60bf6..c18bcae2a 100644 --- a/tests/test_cpu.py +++ b/tests/test_cpu.py @@ -70,9 +70,10 @@ def test_is_psutil_not_available_on_exception(self, mock_cpu_times): @mock.patch("psutil.sensors_temperatures") def psutil_returns_expected_temperature(self, mock_cpu_times): mock_temp = mock.Mock() - mock_temp.return_value = {"coretemp" : 50, "k10temp" : 50, "cpu_thermal" : 50} + mock_temp.return_value = {"coretemp": 50, "k10temp": 50, "cpu_thermal": 50} self.assertEqual(psutil.sensors_temperatures(), 50) + class TestRAPLHelperFunctions(unittest.TestCase): def test_get_candidate_bases_for_custom_dir(self): with tempfile.TemporaryDirectory() as parent: @@ -856,4 +857,3 @@ def test_count_physical_cpus_linux(self): side_effect=subprocess.CalledProcessError(1, "lscpu"), ): assert count_physical_cpus() == 1 - From 5bdf6e6282db881174cb51f44d3b7c98d45e6cba Mon Sep 17 00:00:00 2001 From: Pat3690 <87405996+Pat3690@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:47:21 -0400 Subject: [PATCH 27/29] Updated Documentation, can be found in /docs/Contributions --- docs/Contributions/cputemp.md | 63 ++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/Contributions/cputemp.md b/docs/Contributions/cputemp.md index 8a4574e9f..c656cb258 100644 --- a/docs/Contributions/cputemp.md +++ b/docs/Contributions/cputemp.md @@ -39,8 +39,69 @@ def get_cpu_temperature(self) -> float: logger.debug(f"get_cpu_temperature: Could not read CPU temperature: {e}") return 0.0 ``` +### Added Workflow + +```python +name: Test Temperature Tracking + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + test-temperature: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -e . + pip install pandas + + - name: Check sensors available + run: | + sudo apt-get install -y lm-sensors + python3 -c "import psutil; print('Sensors:', psutil.sensors_temperatures())" + + - name: Run temperature test + run: | + python3 -c " + import time + from codecarbon import EmissionsTracker + + tracker = EmissionsTracker( + project_name='temperature_test', + measure_power_secs=15, + save_to_file=True, + output_file='emissions_temp_test.csv', + log_level='debug' + ) + + tracker.start() + total = sum(range(10_000_000)) + time.sleep(30) + emissions = tracker.stop() + + print(f'Emissions: {emissions:.6f} kg CO2') + print(f'CPU temperature: {tracker.final_emissions_data.cpu_temperature:.1f}C') + print(f'GPU temperature: {tracker.final_emissions_data.gpu_temperature:.1f}C') + + import pandas as pd + df = pd.read_csv('emissions_temp_test.csv') + print('CSV columns:', df.columns.tolist()) + print('Temperature values:') + print(df[['cpu_temperature', 'gpu_temperature']]) + " +``` Allowed for CodeCarbon to track it and input it in to the CSV data set, shown in terminal below ![](../images/CpuTemp.png){.align-center width="700px" height="400px"} -Make sure to run the 'test_temp.py' file From 139711901d9f7ac20f129fae96167153ad6021f2 Mon Sep 17 00:00:00 2001 From: msavas Date: Sun, 19 Apr 2026 16:07:13 -0400 Subject: [PATCH 28/29] rerun tests From 1b2f921f17b41a876c3c946d00ea405cda6c4ed2 Mon Sep 17 00:00:00 2001 From: JargusB Date: Fri, 24 Apr 2026 17:38:57 -0400 Subject: [PATCH 29/29] retrying push --- codecarbon/core/ai_accelerator.py | 0 codecarbon/core/neuron.py | 284 ++++++++++++++++++++ codecarbon/core/resource_tracker.py | 28 +- codecarbon/emissions_tracker.py | 34 ++- codecarbon/external/hardware.py | 45 ++++ codecarbon/output_methods/emissions_data.py | 6 + 6 files changed, 394 insertions(+), 3 deletions(-) delete mode 100644 codecarbon/core/ai_accelerator.py create mode 100644 codecarbon/core/neuron.py diff --git a/codecarbon/core/ai_accelerator.py b/codecarbon/core/ai_accelerator.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/codecarbon/core/neuron.py b/codecarbon/core/neuron.py new file mode 100644 index 000000000..80bfe8741 --- /dev/null +++ b/codecarbon/core/neuron.py @@ -0,0 +1,284 @@ +""" +Implements tracking for AWS Inferentia and Inferentia2 AI accelerator chips +via the Neuron sysfs interface. + +Sysfs power file location: +/sys/devices/virtual/neuron_device/neuron{i}/stats/power/utilization + +Sysfs power file format: +,,,, + +Where power values are percentages (0.00-100.00) of max TDP. +Updated every 60 seconds by the Neuron driver. + +IMPORTANT - Sampling frequency limitation: +The Neuron sysfs power file updates every 60 seconds. +codecarbon reads it every 15 seconds by default, meaning +the same value may be read up to 4 times between updates. + +Impact: +- Steady workloads: minimal impact, power is relatively constant +- Bursty workloads: may miss power spikes between updates +- Runs < 60 seconds: energy estimate may be based on a single sample +- Long runs: averages out over time, impact diminishes + +NOTE: Power is reported at device level, not per-process. +Accurate for exclusive instances, approximate for shared Neuron cores. +""" + +import glob +import os +from typing import Dict, List, Optional, Tuple + +from codecarbon.external.logger import logger + +# Maximum TDP per device type in watts. +# Only Inferentia (inf1) and Inferentia2 (inf2) are currently supported. +# Add other devices when their power specs are properly researched. +# TDP values are approximate and used to estimate watts from utilization%. +NEURON_DEVICE_TDP_WATTS = { + # long format from device_name sysfs file + "inferentia": 75, + "inferentia2": 100, + # shorthand format from instance_type sysfs file + "inf1": 75, + "inf2": 100, +} + + +def is_neuron_system() -> bool: + """ + Check if AWS Inferentia/Inferentia2 Neuron device is available + by checking if the Neuron sysfs directory exists. + Returns True if Neuron devices are present, False otherwise. + """ + return os.path.exists("/sys/devices/virtual/neuron_device") + + +class NeuronDevice: + """ + Represents a single AWS Inferentia/Inferentia2 Neuron device. + + Reads power utilization from Neuron sysfs at: + /sys/devices/virtual/neuron_device/neuron{i}/stats/power/utilization + + Power is reported as a percentage of max TDP, updated every 60 seconds. + Watts are estimated by multiplying utilization% by the device TDP. + + Accuracy limitations: + - Power derived from utilization% x TDP, not directly measured + - sysfs updates every 60 seconds, codecarbon reads every 15 seconds + - Device-level power only, not per-process attribution + - TDP values are approximate, not officially confirmed by AWS + for power tracking purposes + """ + + def __init__(self, device_path: str, device_index: int): + self._device_path = device_path + self._device_index = device_index + self._max_power_watts = self._get_max_power_watts() + + def _get_max_power_watts(self) -> float: + """ + Look up device TDP by reading device_name, instance_type, + or arch_type from the sysfs info directory. + Tries each file in order, returns first match. + Returns 0.0 if device is not supported or file cannot be read. + """ + try: + for filename in ["device_name", "instance_type", "arch_type"]: + path = os.path.join(self._device_path, "info", "architecture", filename) + if not os.path.exists(path): + continue + with open(path, "r") as f: + name = f.read().strip().lower() + tdp = NEURON_DEVICE_TDP_WATTS.get(name, 0.0) + if tdp > 0: + logger.debug( + f"NeuronDevice {self._device_index}: " + f"{filename}='{name}', TDP={tdp}W" + ) + return tdp + else: + logger.warning( + f"NeuronDevice {self._device_index}: " + f"device '{name}' is not currently supported. " + "Only Inferentia (inf1) and Inferentia2 (inf2) " + "are supported. Power will be reported as 0.0W." + ) + return 0.0 + logger.warning( + f"NeuronDevice {self._device_index}: " + "could not determine device type from sysfs info directory." + ) + return 0.0 + except Exception as e: + logger.debug( + f"NeuronDevice {self._device_index}: " + f"could not read device info: {e}" + ) + return 0.0 + + def _read_power_file(self) -> Optional[Tuple[str, float, float, float]]: + """ + Read and parse the Neuron sysfs power utilization file. + + Format: ,,,, + + Returns (status, min_pct, max_pct, avg_pct) or None on error. + """ + try: + power_file = os.path.join( + self._device_path, "stats", "power", "utilization" + ) + if not os.path.exists(power_file): + logger.debug( + f"NeuronDevice {self._device_index}: " + f"power file not found at {power_file}" + ) + return None + + with open(power_file, "r") as f: + content = f.read().strip() + + parts = content.split(",") + if len(parts) != 5: + logger.debug( + f"NeuronDevice {self._device_index}: " + f"unexpected power file format: {content}" + ) + return None + + status, _, min_pct, max_pct, avg_pct = parts + return status, float(min_pct), float(max_pct), float(avg_pct) + + except Exception as e: + logger.debug( + f"NeuronDevice {self._device_index}: " f"could not read power file: {e}" + ) + return None + + def get_utilization_pct(self) -> float: + """ + Returns the raw average power utilization percentage (0.00-100.00) + as reported directly by the Neuron sysfs interface. + This is the direct measured value with no estimation involved. + Returns 0.0 if status is not POWER_STATUS_VALID or on error. + """ + result = self._read_power_file() + if result is None: + return 0.0 + + status, _, _, avg_pct = result + + if status != "POWER_STATUS_VALID": + logger.debug( + f"NeuronDevice {self._device_index}: " + f"power status: {status}, returning 0.0%" + ) + return 0.0 + + logger.debug( + f"NeuronDevice {self._device_index}: " f"utilization={avg_pct:.2f}%" + ) + return avg_pct + + def get_power_watts(self) -> float: + """ + Returns estimated power in watts by multiplying utilization% + by the device TDP. + + NOTE: This is an estimation. For the raw measured value + use get_utilization_pct() instead. + Returns 0.0 if TDP is unknown or status is not POWER_STATUS_VALID. + """ + if self._max_power_watts == 0.0: + logger.debug( + f"NeuronDevice {self._device_index}: " + "TDP unknown, cannot estimate watts" + ) + return 0.0 + + result = self._read_power_file() + if result is None: + return 0.0 + + status, _, _, avg_pct = result + + if status != "POWER_STATUS_VALID": + logger.debug( + f"NeuronDevice {self._device_index}: " + f"power status: {status}, returning 0.0W" + ) + return 0.0 + + watts = (avg_pct / 100.0) * self._max_power_watts + logger.debug( + f"NeuronDevice {self._device_index}: " + f"avg={avg_pct:.2f}%, TDP={self._max_power_watts}W " + f"=> {watts:.2f}W" + ) + return watts + + def get_device_index(self) -> int: + return self._device_index + + +class AllNeuronDevices: + """ + Discovers and manages all AWS Inferentia/Inferentia2 Neuron devices + on the system by scanning the Neuron sysfs directory. + """ + + def __init__(self): + self._devices: List[NeuronDevice] = self._discover_devices() + logger.info(f"Found {len(self._devices)} Neuron device(s)") + + def _discover_devices(self) -> List[NeuronDevice]: + """ + Scan sysfs for Neuron devices and return a sorted list + of NeuronDevice objects. + Uses neuron[0-9]* glob to avoid matching neuron_core directories. + """ + base_path = "/sys/devices/virtual/neuron_device" + device_paths = sorted(glob.glob(os.path.join(base_path, "neuron[0-9]*"))) + devices = [] + for i, path in enumerate(device_paths): + if os.path.isdir(path): + devices.append(NeuronDevice(path, i)) + logger.info(f"Neuron device {i} found at {path}") + return devices + + @property + def device_count(self) -> int: + return len(self._devices) + + def get_total_power_watts(self) -> float: + """ + Sum estimated power in watts across all Neuron devices. + See NeuronDevice.get_power_watts() for accuracy limitations. + """ + return sum(d.get_power_watts() for d in self._devices) + + def get_total_utilization_pct(self) -> float: + """ + Average raw utilization percentage across all Neuron devices. + This is the direct measured value with no estimation involved. + Returns 0.0 if no devices are present. + """ + if not self._devices: + return 0.0 + return sum(d.get_utilization_pct() for d in self._devices) / len(self._devices) + + def get_device_details(self) -> List[Dict]: + """ + Return a list of dicts with per-device power and utilization. + """ + return [ + { + "device_index": d.get_device_index(), + "power_watts": d.get_power_watts(), + "utilization_pct": d.get_utilization_pct(), + } + for d in self._devices + ] diff --git a/codecarbon/core/resource_tracker.py b/codecarbon/core/resource_tracker.py index 67786189d..568ee0ec7 100644 --- a/codecarbon/core/resource_tracker.py +++ b/codecarbon/core/resource_tracker.py @@ -3,6 +3,7 @@ from codecarbon.core import cpu, gpu, powermetrics from codecarbon.core.config import normalize_gpu_ids +from codecarbon.core.neuron import is_neuron_system from codecarbon.core.util import ( detect_cpu_model, is_linux_os, @@ -10,13 +11,19 @@ is_mac_os, is_windows_os, ) -from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip +from codecarbon.external.hardware import ( + CPU, + GPU, + MODE_CPU_LOAD, + AppleSiliconChip, + NeuronChip, +) from codecarbon.external.logger import logger from codecarbon.external.ram import RAM class ResourceTracker: - cpu_tracker = gpu_tracker = ram_tracker = "Unspecified" + cpu_tracker = gpu_tracker = ram_tracker = neuron_tracker = "Unspecified" def __init__(self, tracker): self.tracker = tracker @@ -250,6 +257,21 @@ def set_GPU_tracking(self): self.tracker._conf.setdefault("gpu_count", 0) self.tracker._conf.setdefault("gpu_model", "") + def set_Neuron_tracking(self): + logger.info("[setup] Neuron Tracking...") + if is_neuron_system(): + logger.info("Tracking AWS Inferentia/Inferentia2 via Neuron sysfs") + neuron = NeuronChip() + self.tracker._hardware.append(neuron) + self.tracker._conf["neuron_count"] = neuron._devices.device_count + self.tracker._conf["neuron_model"] = neuron._model + self.neuron_tracker = "Neuron sysfs" + else: + logger.info("No Neuron device found.") + self.tracker._conf.setdefault("neuron_count", 0) + self.tracker._conf.setdefault("neuron_model", "") + self.neuron_tracker = "Unspecified" + def set_CPU_GPU_ram_tracking(self): """ Set up CPU, GPU and RAM tracking based on the user's configuration. @@ -258,11 +280,13 @@ def set_CPU_GPU_ram_tracking(self): self.set_RAM_tracking() self.set_CPU_tracking() self.set_GPU_tracking() + self.set_Neuron_tracking() logger.info( f"""The below tracking methods have been set up: RAM Tracking Method: {self.ram_tracker} CPU Tracking Method: {self.cpu_tracker} GPU Tracking Method: {self.gpu_tracker} + Neuron Tracking Method: {self.neuron_tracker} """ ) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index d2b43c81f..bad2628ce 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -23,7 +23,7 @@ from codecarbon.core.units import Energy, Power, Time, Water from codecarbon.core.util import count_cpus, count_physical_cpus, suppress from codecarbon.external.geography import CloudMetadata, GeoMetadata -from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip +from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip, NeuronChip from codecarbon.external.logger import logger, set_logger_format, set_logger_level from codecarbon.external.ram import RAM from codecarbon.external.scheduler import PeriodicScheduler @@ -376,6 +376,10 @@ def __init__( self._cpu_power: Power = Power.from_watts(watts=0) self._gpu_power: Power = Power.from_watts(watts=0) self._ram_power: Power = Power.from_watts(watts=0) + self._total_neuron_energy: Energy = Energy.from_energy(kWh=0) + self._neuron_power: Power = Power.from_watts(watts=0) + self._neuron_power_sum: float = 0.0 + self._neuron_utilization_history: List[float] = [] # Running average tracking for power self._cpu_power_sum: float = 0.0 self._gpu_power_sum: float = 0.0 @@ -552,6 +556,7 @@ def start(self) -> None: self._gpu_utilization_history.clear() self._cpu_temperature_history.clear() self._gpu_temperature_history.clear() + self._neuron_utilization_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -604,6 +609,7 @@ def start_task(self, task_name=None) -> None: self._gpu_utilization_history.clear() self._cpu_temperature_history.clear() self._gpu_temperature_history.clear() + self._neuron_utilization_history.clear() # Read initial energy for hardware for hardware in self._hardware: @@ -938,6 +944,18 @@ def _prepare_emissions_data(self) -> EmissionsData: if self._gpu_temperature_history else 0.0 ), + neuron_power=( + self._neuron_power_sum / self._power_measurement_count + if self._power_measurement_count > 0 + else self._neuron_power.W + ), + neuron_energy=self._total_neuron_energy.kWh, + neuron_utilization_pct=( + sum(self._neuron_utilization_history) + / len(self._neuron_utilization_history) + if self._neuron_utilization_history + else 0.0 + ), ) logger.debug(total_emissions) return total_emissions @@ -1012,6 +1030,12 @@ def _monitor_power(self) -> None: gpu_detail["temperature"] ) + for hardware in self._hardware: + if isinstance(hardware, NeuronChip): + self._neuron_utilization_history.append( + hardware._devices.get_total_utilization_pct() + ) + def _do_measurements(self) -> None: for hardware in self._hardware: h_time = time.perf_counter() @@ -1076,6 +1100,14 @@ def _do_measurements(self) -> None: f"Energy consumed for all AppleSilicon GPUs : {self._total_gpu_energy.kWh:.6f} kWh" + f". Total GPU Power : {self._gpu_power.W} W" ) + elif isinstance(hardware, NeuronChip): + self._total_neuron_energy += energy + self._neuron_power = power + self._neuron_power_sum += power.W + logger.info( + f"Energy consumed for Neuron : {self._total_neuron_energy.kWh:.6f} kWh" + + f". Neuron Power : {self._neuron_power.W} W" + ) else: logger.error(f"Unknown hardware type: {hardware} ({type(hardware)})") h_time = time.perf_counter() - h_time diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index fa0ac50cf..10b194222 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -13,6 +13,7 @@ from codecarbon.core.cpu import IntelPowerGadget, IntelRAPL from codecarbon.core.gpu import AllGPUDevices +from codecarbon.core.neuron import AllNeuronDevices from codecarbon.core.powermetrics import ApplePowermetrics from codecarbon.core.units import Energy, Power, Time from codecarbon.core.util import count_cpus, detect_cpu_model @@ -556,3 +557,47 @@ def from_utils( logger.warning("Could not read AppleSiliconChip model.") return cls(output_dir=output_dir, model=model, chip_part=chip_part) + + +@dataclass +class NeuronChip(BaseHardware): + """ + Tracks AWS Inferentia/Inferentia2 power consumption + via the Neuron sysfs interface. + + Power is estimated from utilization% x TDP. + Utilization% is the raw measured value from sysfs. + + Sampling limitation: Neuron sysfs updates every 60 seconds. + codecarbon reads every 15 seconds so the same value may be + read up to 4 times between updates. Energy estimates are most + accurate for steady workloads and runs longer than 60 seconds. + + NOTE: Neuron sysfs reports device-level power, not per-process. + Accurate for exclusive instances, approximate for shared Neuron cores. + """ + + def __init__(self): + self._devices = AllNeuronDevices() + self._model = "AWS Inferentia/Inferentia2" + logger.warning( + "Neuron power sysfs updates every 60 seconds. " + "codecarbon reads every 15 seconds so power readings " + "may be stale between updates. Energy estimates are most " + "accurate for runs longer than 60 seconds with steady workloads." + ) + + def __repr__(self) -> str: + return f"NeuronChip({self._model}, " f"{self._devices.device_count} device(s))" + + def total_power(self) -> Power: + """ + Returns total estimated power across all Neuron devices in watts. + Called every 15 seconds by _do_measurements() in tracker.py. + Power is estimated from utilization% x TDP. + """ + watts = self._devices.get_total_power_watts() + return Power.from_watts(watts) + + def description(self) -> str: + return repr(self) diff --git a/codecarbon/output_methods/emissions_data.py b/codecarbon/output_methods/emissions_data.py index b086f3eca..38e2a92b0 100644 --- a/codecarbon/output_methods/emissions_data.py +++ b/codecarbon/output_methods/emissions_data.py @@ -49,6 +49,9 @@ class EmissionsData: wue: float = 0 cpu_temperature: float = 0.0 # ADD gpu_temperature: float = 0.0 # ADD + neuron_power: float = 0.0 + neuron_energy: float = 0.0 + neuron_utilization_pct: float = 0.0 @property def values(self) -> OrderedDict: @@ -114,6 +117,9 @@ class TaskEmissionsData: on_cloud: str = "N" cpu_temperature: float = 0.0 gpu_temperature: float = 0.0 + neuron_power: float = 0.0 + neuron_energy: float = 0.0 + neuron_utilization_pct: float = 0.0 @property def values(self) -> OrderedDict: