Skip to content

[Version 3] Raspberry CPU support #791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 114 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
3164f81
build on codecarbon_v3_rc
Dec 11, 2024
4a2b6ae
bump
Dec 11, 2024
35ef095
hatch run dev:bumpver update --set-version 3.0.0-rc0 --tag-num
Dec 11, 2024
311055e
build
Dec 11, 2024
c956f21
Add CPU load tracking
benoit-cty Jun 13, 2022
01e1d27
Add new CPU
benoit-cty Jun 14, 2022
2e2a013
Add mandatory country
benoit-cty Jun 14, 2022
4a867dc
Fix RAPL fallback
benoit-cty Jun 14, 2022
36f338c
debug RAPL
Nov 26, 2024
602de48
Release Drafter
Dec 11, 2024
5c3def8
refacto ResourceTracker
Dec 11, 2024
1ccf6b9
example
Dec 12, 2024
fee6837
Add CPU load
Jan 7, 2025
556b16b
Add division to Power
Jan 7, 2025
83dc626
Handle Pandas warning
Jan 7, 2025
0ba2fb9
CPU Load task support
Jan 7, 2025
4cbfe4e
CPU load and RAPL comparison
Jan 7, 2025
d07db89
Better cpu load estimation
Jan 8, 2025
463740c
change version
Jan 10, 2025
35d0040
Fix test test_carbon_tracker_offline_region_error
Jan 10, 2025
cb0e8df
RAPL path
Jan 10, 2025
53faaec
Handle no Tapo
Jan 10, 2025
8f78d5d
Debug config
Jan 10, 2025
ed52589
wip: test on Scaleway
Jan 10, 2025
c16e06f
Add load per core
Jan 10, 2025
0bb2cc8
Add E5-2620 v3
Jan 11, 2025
84eac1c
Add a TDP test
Jan 11, 2025
9664fb4
Fix match for Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz
Jan 11, 2025
d23c919
Fix psutil detection
Jan 11, 2025
5ff761f
Fix cpu 0
Jan 11, 2025
27893f9
Scaleway test results
Jan 12, 2025
d5e122f
TO REVERT : publish RC to Pypi
Jan 12, 2025
fa12978
Notes on E5-2620
Jan 12, 2025
937bcf9
Add count_physical_cpus
Jan 14, 2025
1841fe3
Multiply TDP per physical CPU
Jan 14, 2025
98d70b8
Custom CPU load
Jan 14, 2025
316ef7e
Find RAM domain
Jan 14, 2025
2e05544
Handle RAPL RAM
Jan 14, 2025
d2e2b97
Improve RAPL
Jan 14, 2025
fed96ee
Do not read core
Jan 14, 2025
cddc72a
Docs
Jan 14, 2025
5674f13
CPU Fallback doc
Jan 17, 2025
5d5a09a
AMD CPU
Feb 3, 2025
a9d90e0
Add W per core for default
benoit-cty Feb 4, 2025
afff566
Data on CPU loads
Feb 14, 2025
e90ed10
fix circular import
Feb 15, 2025
57e5fa1
Default to 1 CPU
Feb 15, 2025
a07e44b
Fix case TDP is None
Feb 15, 2025
bf0dd10
bump
Feb 15, 2025
3bf3af6
docs
Feb 15, 2025
9674f05
Merge pull request #316 from mlco2/use-cpu-load
benoit-cty Feb 15, 2025
d760d63
fix: remove percpu argument for psutil.cpu_percent & process.cpu_perc…
SaboniAmine Feb 17, 2025
16d15c1
remove percpu
Feb 18, 2025
719c22d
Set minimal version for psutil
Feb 18, 2025
455bddd
bump deps
Feb 18, 2025
42f9038
bump to rc2
Feb 18, 2025
2faf374
Fix _get_power_from_cpu_load in process mode
Feb 18, 2025
63b2994
Fix pandas warning
Feb 18, 2025
f346008
Merge pull request #786 from mlco2/fix/remove_deprecated_psutil_percpu
benoit-cty Feb 18, 2025
7552636
Add AMD EPYC 7R13
benoit-cty Feb 20, 2025
d4f5a76
Fix percentage
Feb 20, 2025
9200015
bump
Feb 20, 2025
ab080e6
Merge pull request #787 from mlco2/cpu_AMD_EPYC_7R13
benoit-cty Feb 20, 2025
a760db8
feat(api): add auth to project token endpoints (#746)
inimaz Jan 18, 2025
06a5ee5
feat(carbonserver) allow accessing public project data
prmths128 Jan 14, 2025
d983019
chore(deps): bump next from 14.2.15 to 14.2.21 in /webapp (#762)
dependabot[bot] Jan 19, 2025
66a3831
feat: update project settings (name +description) (#761)
inimaz Jan 19, 2025
7749bae
fix: redirect user to dashboard after cli account creation (#763)
SaboniAmine Jan 19, 2025
039ddbc
chore: bump version (#765)
SaboniAmine Jan 19, 2025
b2ec679
Create FUNDING.yml
benoit-cty Jan 22, 2025
fac18b6
Improve arguments consistency in track_emissions decorator
LucasHaug Jan 22, 2025
097878c
feat: allow_multiple_runs is true by default
inimaz Feb 6, 2025
b2315a7
ci: deploy app from ci (#772)
inimaz Jan 26, 2025
718ebf5
deps bump
benoit-cty Jan 28, 2025
80c634c
Check if tracker iniialized.
benoit-cty Jan 28, 2025
606f0da
Fix empty country
Jan 29, 2025
c75b5e3
Bump deps
Jan 29, 2025
4b5ea69
Fix(lock): SIGTERM handling to allow cleanup code execution (#785)
Ucodia Feb 13, 2025
5330cbd
fix(api): auth context can read project
inimaz Mar 8, 2025
b1844c0
fix: unit test
inimaz Mar 8, 2025
6286fd2
fix: minor change in log that says where the codecarbon output is (#794)
inimaz Mar 9, 2025
3470d55
chore(carbonserver) remove unused users endpoint
prmths128 Mar 9, 2025
6d2d9ec
Add Intel Core Ultra 7 165H
FrejaThoresen Mar 20, 2025
a2748c5
fix: update nextjs to latest to prevent known vulnerability
inimaz Mar 25, 2025
f53ab98
chores: split github workflows (#812)
inimaz Apr 1, 2025
d39a1cb
Feat: create new experiment from the webapp (#816)
inimaz Apr 4, 2025
edbded8
chore(deps): bump @babel/runtime from 7.24.8 to 7.27.0 in /webapp (#811)
dependabot[bot] Apr 4, 2025
9a8aa47
feat(ui): share project page (#818)
alexphiev Apr 11, 2025
289ba32
Update cpu_power.csv
benoit-cty Apr 11, 2025
e902c4b
fix line feed
Apr 13, 2025
d4fea24
rebuild docs
Apr 13, 2025
344feb6
bump deps
Apr 13, 2025
1bc0982
Merge pull request #780 from mlco2/feat/allow-multiple-runs-true
benoit-cty Apr 13, 2025
fb01a32
New heuristic for RAM
Mar 15, 2025
0918e52
Fix test and RAM slot power
Mar 15, 2025
a13db61
force ram and cpu power
Mar 28, 2025
c6d6a9d
tests
Mar 28, 2025
c02ae05
force_ram_power
Apr 2, 2025
ff5e4ff
seems to work
Apr 2, 2025
eb53e7a
Fix test
Apr 6, 2025
56e6aa2
bump and docs
Apr 6, 2025
0b8e6b3
doc
Apr 6, 2025
3bfc92c
fix bug with psutil detection
Apr 11, 2025
4f53a13
bump
Apr 11, 2025
4ee7644
Add a minimum of 10% TDP
Apr 11, 2025
d20b71b
Log modes to INFO
Apr 11, 2025
40c0af8
bump
Apr 11, 2025
4c4fe80
fix max
Apr 11, 2025
38fa473
Typo
Apr 12, 2025
aafd5f6
typo
Apr 12, 2025
90d6ec9
bump deps
Apr 13, 2025
b3e827d
doc
Apr 13, 2025
8eac358
Merge pull request #804 from mlco2/feat/ram_stick_power
benoit-cty Apr 13, 2025
d27aa53
rpi 4 and 5 cpu
Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix test and RAM slot power
  • Loading branch information
benoit-cty committed Apr 13, 2025
commit 0918e52529bb5e30c5be16d1c6a07f92df6b60ca
3 changes: 2 additions & 1 deletion codecarbon/core/resource_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from codecarbon.core import cpu, gpu, powermetrics
from codecarbon.core.config import parse_gpu_ids
from codecarbon.core.util import detect_cpu_model, is_linux_os, is_mac_os, is_windows_os
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, RAM, AppleSiliconChip
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip
from codecarbon.external.logger import logger
from codecarbon.external.ram import RAM


class ResourceTracker:
Expand Down
3 changes: 2 additions & 1 deletion codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
from codecarbon.core.units import Energy, Power, Time
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, RAM, AppleSiliconChip
from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip
from codecarbon.external.logger import logger, set_logger_format, set_logger_level
from codecarbon.external.ram import RAM
from codecarbon.external.scheduler import PeriodicScheduler
from codecarbon.external.task import Task
from codecarbon.input import DataSource
Expand Down
338 changes: 1 addition & 337 deletions codecarbon/external/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import math
import re
import subprocess
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Tuple
Expand All @@ -15,7 +14,7 @@
from codecarbon.core.gpu import AllGPUDevices
from codecarbon.core.powermetrics import ApplePowermetrics
from codecarbon.core.units import Energy, Power, Time
from codecarbon.core.util import SLURM_JOB_ID, count_cpus, detect_cpu_model
from codecarbon.core.util import count_cpus, detect_cpu_model
from codecarbon.external.logger import logger

# default W value for a CPU if no model is found in the ref csv
Expand All @@ -28,8 +27,6 @@

MODE_CPU_LOAD = "cpu_load"

RAM_SLOT_POWER_X86 = 4 # Watts


@dataclass
class BaseHardware(ABC):
Expand Down Expand Up @@ -333,339 +330,6 @@ def from_utils(
)


@dataclass
class RAM(BaseHardware):
"""
Before V3 heuristic:
# 3 watts of power for every 8GB of DDR3 or DDR4 memory
# https://www.crucial.com/support/articles-faq-memory/how-much-power-does-memory-use

In V3, we need to improve the accuracy of the RAM power estimation.
Because the power consumption of RAM is not linear with the amount of memory used,
for example, in servers you could have thousands of GB of RAM but the power
consumption would not be proportional to the amount of memory used, but to the number
of memory modules used.
But there is no way to know the memory modules used in the system, without admin rights.
So we need to build a heuristic that is more accurate than the previous one.
For example keep a minimum of 2 modules. Execept for ARM CPU like rapsberry pi where we will consider a 3W constant.
Then consider the max RAM per module is 128GB and that RAM module only exist in power of 2 (2, 4, 8, 16, 32, 64, 128).
So we can estimate the power consumption of the RAM by the number of modules used.

1. **ARM CPU Detection**:
- Added a `_detect_arm_cpu` method that checks if the system is using an ARM architecture
- For ARM CPUs (like Raspberry Pi), a constant 3W will be used as the minimum power

2. **DIMM Count Estimation**:
- Created a `_estimate_dimm_count` method that intelligently estimates how many memory modules might be present based on total RAM size
- Takes into account that servers typically have more and larger DIMMs
- Assumes DIMM sizes follow powers of 2 (4GB, 8GB, 16GB, 32GB, 64GB, 128GB) as specified

3. **Scaling Power Model**:
- Base power per DIMM is 2.5W for x86 systems and 1.5W for ARM systems
- For standard systems (up to 4 DIMMs): linear scaling at full power per DIMM
- For medium systems (5-8 DIMMs): decreasing efficiency (90% power per additional DIMM)
- For large systems (9-16 DIMMs): further reduced efficiency (80% power per additional DIMM)
- For very large systems (17+ DIMMs): highest efficiency (70% power per additional DIMM)

4. **Minimum Power Guarantees**:
- Ensures at least 5W for x86 systems (assuming 2 DIMMs at minimum)
- Ensures at least 3W for ARM systems as requested

### Example Power Estimates:

- **Small laptop (8GB RAM)**: ~5W (2 DIMMs at 2.5W each)
- **Desktop (32GB RAM)**: ~10W (4 DIMMs at 2.5W each)
- **Small server (128GB RAM)**: ~18.6W (8 DIMMs with efficiency scaling)
- **Large server (1TB RAM)**: ~44W (using 16x64GB DIMMs with high efficiency scaling)

This approach significantly improves the accuracy for large servers by recognizing that RAM power consumption doesn't scale linearly with capacity, but rather with the number of physical modules. Since we don't have direct access to the actual DIMM configuration, this heuristic provides a more reasonable estimate than the previous linear model.

The model also includes detailed debug logging that will show the estimated power for given memory sizes, helping with validation and fine-tuning in the future.
"""

memory_size = None
is_arm_cpu = False

def __init__(
self,
pid: int = psutil.Process().pid,
children: bool = True,
tracking_mode: str = "machine",
):
"""
Instantiate a RAM object from a reference pid. If none is provided, will use the
current process's. The `pid` is used to find children processes if `children`
is True.

Args:
pid (int, optional): Process id (with respect to which we'll look for
children). Defaults to psutil.Process().pid.
children (int, optional): Look for children of the process when computing
total RAM used. Defaults to True.
"""
self._pid = pid
self._children = children
self._tracking_mode = tracking_mode
# Check if using ARM architecture
self.is_arm_cpu = self._detect_arm_cpu()

def _detect_arm_cpu(self) -> bool:
"""
Detect if the CPU is ARM-based
"""
try:
# Try to detect ARM architecture using platform module
import platform

machine = platform.machine().lower()
return any(arm in machine for arm in ["arm", "aarch"])
except Exception:
# Default to False if detection fails
return False

def _estimate_dimm_count(self, total_gb: float) -> int:
"""
Estimate the number of memory DIMMs based on total memory size
using heuristic rules.

Args:
total_gb: Total RAM in GB

Returns:
int: Estimated number of memory DIMMs
"""
# Typical DIMM sizes in GB
dimm_sizes = [4, 8, 16, 32, 64, 128]

# For very small amounts of RAM (e.g. embedded systems)
if total_gb <= 2:
return 1

# For standard desktop/laptop (4-32GB)
if total_gb <= 32:
# Estimate based on likely configurations (2-4 DIMMs)
return max(2, min(4, int(total_gb / 8) + 1))

# For workstations and small servers (32-128GB)
if total_gb <= 128:
# Likely 4-8 DIMMs
return max(4, min(8, int(total_gb / 16) + 1))

# For larger servers (>128GB)
# Estimate using larger DIMM sizes and more slots
# Most servers have 8-32 DIMM slots
# Try to find the best fit with common DIMM sizes
dimm_count = 8 # Minimum for a large server

# Find the largest common DIMM size that fits
for dimm_size in sorted(dimm_sizes, reverse=True):
if dimm_size <= total_gb / 8: # Assume at least 8 DIMMs
# Calculate how many DIMMs of this size would be needed
dimm_count = math.ceil(total_gb / dimm_size)
# Cap at 32 DIMMs (very large server)
dimm_count = min(dimm_count, 32)
break

return dimm_count

def _calculate_ram_power(self, memory_gb: float) -> float:
"""
Calculate RAM power consumption based on the total RAM size using a more
sophisticated model that better scales with larger memory sizes.

Args:
memory_gb: Total RAM in GB

Returns:
float: Estimated power consumption in watts
"""
# Detect how many DIMMs might be present
dimm_count = self._estimate_dimm_count(memory_gb)

# Base power consumption per DIMM
if self.is_arm_cpu:
# ARM systems typically use lower power memory
base_power_per_dimm = 1.5 # Watts
# Minimum 3W for ARM as requested
min_power = 3.0
else:
# x86 systems
base_power_per_dimm = RAM_SLOT_POWER_X86 # Watts
# Minimum 5W for x86 as requested (2 sticks at 2.5W)
min_power = base_power_per_dimm * 2

# Estimate power based on DIMM count with decreasing marginal power per DIMM as count increases
if dimm_count <= 4:
# Small systems: full power per DIMM
total_power = base_power_per_dimm * dimm_count
elif dimm_count <= 8:
# Medium systems: slight efficiency at scale
total_power = base_power_per_dimm * 4 + base_power_per_dimm * 0.9 * (
dimm_count - 4
)
elif dimm_count <= 16:
# Larger systems: better efficiency at scale
total_power = (
base_power_per_dimm * 4
+ base_power_per_dimm * 0.9 * 4
+ base_power_per_dimm * 0.8 * (dimm_count - 8)
)
else:
# Very large systems: high efficiency at scale
total_power = (
base_power_per_dimm * 4
+ base_power_per_dimm * 0.9 * 4
+ base_power_per_dimm * 0.8 * 8
+ base_power_per_dimm * 0.7 * (dimm_count - 16)
)

# Apply minimum power constraint
return max(min_power, total_power)

def _get_children_memories(self):
"""
Compute the used RAM by the process's children

Returns:
list(int): The list of RAM values
"""
current_process = psutil.Process(self._pid)
children = current_process.children(recursive=True)
return [child.memory_info().rss for child in children]

def _read_slurm_scontrol(self):
try:
logger.debug(
"SLURM environment detected, running `scontrol show job $SLURM_JOB_ID`..."
)
return (
subprocess.check_output(
[f"scontrol show job {SLURM_JOB_ID}"], shell=True
)
.decode()
.strip()
)
except subprocess.CalledProcessError:
return

def _parse_scontrol_memory_GB(self, mem):
"""
Parse the memory string (B) returned by scontrol to a float (GB)

Args:
mem (str): Memory string (B) as `[amount][unit]` (e.g. `128G`)

Returns:
float: Memory (GB)
"""
nb = int(mem[:-1])
unit = mem[-1]
if unit == "T":
return nb * 1000
if unit == "G":
return nb
if unit == "M":
return nb / 1000
if unit == "K":
return nb / (1000**2)

def _parse_scontrol(self, scontrol_str):
mem_matches = re.findall(r"AllocTRES=.*?,mem=(\d+[A-Z])", scontrol_str)
if len(mem_matches) == 0:
# Try with TRES, see https://github.com/mlco2/codecarbon/issues/569#issuecomment-2167706145
mem_matches = re.findall(r"TRES=.*?,mem=(\d+[A-Z])", scontrol_str)
if len(mem_matches) == 0:
logger.warning(
"Could not find mem= after running `scontrol show job $SLURM_JOB_ID` "
+ "to count SLURM-available RAM. Using the machine's total RAM."
)
return psutil.virtual_memory().total / B_TO_GB
if len(mem_matches) > 1:
logger.warning(
"Unexpected output after running `scontrol show job $SLURM_JOB_ID` "
+ "to count SLURM-available RAM. Using the machine's total RAM."
)
return psutil.virtual_memory().total / B_TO_GB

return mem_matches[0].replace("mem=", "")

@property
def slurm_memory_GB(self):
"""
Property to compute the SLURM-available RAM in GigaBytes.

Returns:
float: Memory allocated to the job (GB)
"""
# Prevent calling scontrol at each mesure
if self.memory_size:
return self.memory_size
scontrol_str = self._read_slurm_scontrol()
if scontrol_str is None:
logger.warning(
"Error running `scontrol show job $SLURM_JOB_ID` "
+ "to retrieve SLURM-available RAM."
+ "Using the machine's total RAM."
)
return psutil.virtual_memory().total / B_TO_GB
mem = self._parse_scontrol(scontrol_str)
if isinstance(mem, str):
mem = self._parse_scontrol_memory_GB(mem)
self.memory_size = mem
return mem

@property
def process_memory_GB(self):
"""
Property to compute the process's total memory usage in bytes.

Returns:
float: RAM usage (GB)
"""
children_memories = self._get_children_memories() if self._children else []
main_memory = psutil.Process(self._pid).memory_info().rss
memories = children_memories + [main_memory]
return sum([m for m in memories if m] + [0]) / B_TO_GB

@property
def machine_memory_GB(self):
"""
Property to compute the machine's total memory in bytes.

Returns:
float: Total RAM (GB)
"""
return (
self.slurm_memory_GB
if SLURM_JOB_ID
else psutil.virtual_memory().total / B_TO_GB
)

def total_power(self) -> Power:
"""
Compute the Power (kW) consumed by the current process (and its children if
`children` was True in __init__)

Returns:
Power: kW of power consumption, using a more sophisticated power model
"""
try:
memory_GB = (
self.machine_memory_GB
if self._tracking_mode == "machine"
else self.process_memory_GB
)
ram_power = Power.from_watts(self._calculate_ram_power(memory_GB))
logger.debug(
f"RAM power estimation: {ram_power.W:.2f}W for {memory_GB:.2f}GB"
)
except Exception as e:
logger.warning(f"Could not measure RAM Power ({str(e)})")
ram_power = Power.from_watts(0)

return ram_power


@dataclass
class AppleSiliconChip(BaseHardware):
def __init__(
Expand Down
Loading