diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f35db2e27..cf742410e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -258,7 +258,7 @@ flake8...................................................................Passed
If any of the linters/formatters fail, check the difference with `git diff`, add the differences if there is no behavior changes (isort and black might have change some coding style or import order, this is expected it is their job) with `git add` and finally try to commit again `git commit ...`.
-You can also run `pre-commit` with `uv run pre-commit run -v` if you have some changes staged but you are not ready yet to commit.
+You can also run `pre-commit` with `uv run pre-commit run --all-file` to check all file.
@@ -363,6 +363,18 @@ cp /data/tests/test_package_integrity.py .
pytest test_package_integrity.py
```
+### Contribute to a fork branch
+
+When a user open a PR from a fork, we are allowed to push to the fork branch.
+
+If you want to do so, do the following:
+
+```bash
+git remote add https://github.com//codecarbon.git
+git fetch
+git checkout -b /
+```
+
## API and Dashboard
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 7fd097b45..ead900903 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -345,7 +345,8 @@ def monitor(
if offline:
if not country_iso_code:
print(
- "ERROR: country_iso_code is required for offline mode", file=sys.stderr
+ "ERROR: Country ISO code is required for offline mode. Add it to your configuration or provide it via the command line: `--country-iso-code FRA`",
+ file=sys.stderr,
)
raise typer.Exit(1)
@@ -358,7 +359,7 @@ def monitor(
experiment_id = get_existing_local_exp_id()
if api and experiment_id is None:
print(
- "ERROR: No experiment id, call 'codecarbon config' first.",
+ "ERROR: No experiment id, call 'codecarbon config' first. Or run in offline mode with `--offline --country-iso-code FRA` flag if you don't want to connect to the API.",
file=sys.stderr,
)
raise typer.Exit(1)
diff --git a/codecarbon/core/config.py b/codecarbon/core/config.py
index 1aa0dd324..7cacea41d 100644
--- a/codecarbon/core/config.py
+++ b/codecarbon/core/config.py
@@ -1,7 +1,7 @@
import configparser
import os
from pathlib import Path
-from typing import List, Union
+from typing import List, Optional, Union
from codecarbon.external.logger import logger
@@ -73,6 +73,43 @@ def parse_gpu_ids(gpu_ids: Union[str, List[int]]) -> List[str]:
)
+def normalize_gpu_ids(
+ gpu_ids: Optional[Union[str, List[Union[int, str]]]],
+) -> Optional[List[Union[int, str]]]:
+ """
+ Normalize GPU IDs from config/user input into a list of ids consumable by hardware
+ resolution code.
+
+ Supports:
+ - comma-separated string values (sanitized via parse_gpu_ids)
+ - lists containing ints and/or strings
+ """
+ if gpu_ids is None:
+ return None
+
+ if isinstance(gpu_ids, str):
+ return parse_gpu_ids(gpu_ids)
+
+ if isinstance(gpu_ids, list):
+ normalized_gpu_ids: List[Union[int, str]] = []
+ for gpu_id in gpu_ids:
+ if isinstance(gpu_id, int):
+ normalized_gpu_ids.append(gpu_id)
+ elif isinstance(gpu_id, str):
+ normalized_gpu_ids.extend(parse_gpu_ids(gpu_id))
+ else:
+ logger.warning(
+ "Ignoring invalid gpu_id entry of type %s; expected int or str.",
+ type(gpu_id).__name__,
+ )
+ return normalized_gpu_ids
+
+ logger.warning(
+ "Invalid gpu_ids format. Expected a string or a list of ints/strings."
+ )
+ return None
+
+
def get_hierarchical_config():
"""
Get the user-defined codecarbon configuration ConfigParser dictionnary
diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py
index 9ed09d20a..74549a14f 100644
--- a/codecarbon/core/cpu.py
+++ b/codecarbon/core/cpu.py
@@ -17,7 +17,7 @@
from codecarbon.core.rapl import RAPLFile
from codecarbon.core.units import Time
-from codecarbon.core.util import detect_cpu_model
+from codecarbon.core.util import count_cpus, detect_cpu_model
from codecarbon.external.logger import logger
from codecarbon.input import DataSource
@@ -1001,7 +1001,7 @@ def _main(self) -> Tuple[str, int]:
)
if is_psutil_available():
# Count thread of the CPU
- threads = psutil.cpu_count(logical=True)
+ threads = count_cpus()
estimated_tdp = threads * DEFAULT_POWER_PER_CORE
logger.warning(
f"We will use the default power consumption of {DEFAULT_POWER_PER_CORE} W per thread for your {threads} CPU, so {estimated_tdp}W."
diff --git a/codecarbon/core/gpu.py b/codecarbon/core/gpu.py
index 2ff55caab..86dd8234f 100644
--- a/codecarbon/core/gpu.py
+++ b/codecarbon/core/gpu.py
@@ -1,223 +1,85 @@
-from dataclasses import dataclass, field
-from typing import Any, Dict, List, Union
+from typing import List
-import pynvml
-
-from codecarbon.core.units import Energy, Power, Time
+from codecarbon.core import gpu_amd, gpu_nvidia
+from codecarbon.core.gpu_device import GPUDevice
+from codecarbon.core.units import Time
from codecarbon.external.logger import logger
+AMDSMI_AVAILABLE = gpu_amd.AMDSMI_AVAILABLE
+PYNVML_AVAILABLE = gpu_nvidia.PYNVML_AVAILABLE
-@dataclass
-class GPUDevice:
- """
- Represents a GPU device with associated energy and power metrics.
-
- Attributes:
- handle (any): An identifier for the GPU device.
- gpu_index (int): The index of the GPU device in the system.
- energy_delta (Energy): The amount of energy consumed by the GPU device
- since the last measurement, expressed in kilowatt-hours (kWh).
- Defaults to an initial value of 0 kWh.
- power (Power): The current power consumption of the GPU device,
- measured in watts (W). Defaults to an initial value of 0 W.
- last_energy (Energy): The last recorded energy reading for the GPU
- device, expressed in kilowatt-hours (kWh). This is used to
- calculate `energy_delta`. Defaults to an initial value of 0 kWh.
- """
-
- handle: any
- gpu_index: int
- # Energy consumed in kWh
- energy_delta: Energy = field(default_factory=lambda: Energy(0))
- # Power based on reading
- power: Power = field(default_factory=lambda: Power(0))
- # Last energy reading in kWh
- last_energy: Energy = field(default_factory=lambda: Energy(0))
-
- def start(self) -> None:
- self.last_energy = self._get_energy_kwh()
-
- def __post_init__(self) -> None:
- self.last_energy = self._get_energy_kwh()
- self._init_static_details()
-
- def _get_energy_kwh(self) -> Energy:
- total_energy_consumption = self._get_total_energy_consumption()
- if total_energy_consumption is None:
- return self.last_energy
- return Energy.from_millijoules(total_energy_consumption)
-
- def delta(self, duration: Time) -> dict:
- """
- Compute the energy/power used since last call.
- """
- new_last_energy = energy = self._get_energy_kwh()
- self.power = self.power.from_energies_and_delay(
- energy, self.last_energy, duration
- )
- self.energy_delta = energy - self.last_energy
- self.last_energy = new_last_energy
- return {
- "name": self._gpu_name,
- "uuid": self._uuid,
- "delta_energy_consumption": self.energy_delta,
- "power_usage": self.power,
- }
-
- def get_static_details(self) -> Dict[str, Any]:
- return {
- "name": self._gpu_name,
- "uuid": self._uuid,
- "total_memory": self._total_memory,
- "power_limit": self._power_limit,
- "gpu_index": self.gpu_index,
- }
-
- def _init_static_details(self) -> None:
- self._gpu_name = self._get_gpu_name()
- self._uuid = self._get_uuid()
- self._power_limit = self._get_power_limit()
- # Get the memory
- memory = self._get_memory_info()
- self._total_memory = memory.total
-
- def get_gpu_details(self) -> Dict[str, Any]:
- # Memory
- memory = self._get_memory_info()
-
- device_details = {
- "name": self._gpu_name,
- "uuid": self._uuid,
- "free_memory": memory.free,
- "total_memory": memory.total,
- "used_memory": memory.used,
- "temperature": self._get_temperature(),
- "power_usage": self._get_power_usage(),
- "power_limit": self._power_limit,
- "total_energy_consumption": self._get_total_energy_consumption(),
- "gpu_utilization": self._get_gpu_utilization(),
- "compute_mode": self._get_compute_mode(),
- "compute_processes": self._get_compute_processes(),
- "graphics_processes": self._get_graphics_processes(),
- }
- return device_details
-
- def _to_utf8(self, str_or_bytes) -> Any:
- if hasattr(str_or_bytes, "decode"):
- return str_or_bytes.decode("utf-8", errors="replace")
-
- return str_or_bytes
-
- def _get_total_energy_consumption(self) -> int:
- """Returns total energy consumption for this GPU in millijoules (mJ) since the driver was last reloaded
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g732ab899b5bd18ac4bfb93c02de4900a
- """
- try:
- return pynvml.nvmlDeviceGetTotalEnergyConsumption(self.handle)
- except pynvml.NVMLError:
- logger.warning(
- "Failed to retrieve gpu total energy consumption", exc_info=True
- )
- return None
-
- def _get_gpu_name(self) -> Any:
- """Returns the name of the GPU device
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1ga5361803e044c6fdf3b08523fb6d1481
- """
- try:
- name = pynvml.nvmlDeviceGetName(self.handle)
- return self._to_utf8(name)
- except UnicodeDecodeError:
- return "Unknown GPU"
-
- def _get_uuid(self) -> Any:
- """Returns the globally unique GPU device UUID
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g72710fb20f30f0c2725ce31579832654
- """
- uuid = pynvml.nvmlDeviceGetUUID(self.handle)
- return self._to_utf8(uuid)
-
- def _get_memory_info(self):
- """Returns memory info in bytes
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g2dfeb1db82aa1de91aa6edf941c85ca8
- """
- try:
- return pynvml.nvmlDeviceGetMemoryInfo(self.handle)
- except pynvml.NVMLError_NotSupported:
- # error thrown for the NVIDIA Blackwell GPU of DGX Spark, due to memory sharing -> return defaults instead
- return pynvml.c_nvmlMemory_t(-1, -1, -1)
-
- def _get_temperature(self) -> int:
- """Returns degrees in the Celsius scale
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g92d1c5182a14dd4be7090e3c1480b121
- """
- return pynvml.nvmlDeviceGetTemperature(self.handle, pynvml.NVML_TEMPERATURE_GPU)
-
- def _get_power_usage(self) -> int:
- """Returns power usage in milliwatts
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g7ef7dff0ff14238d08a19ad7fb23fc87
- """
- return pynvml.nvmlDeviceGetPowerUsage(self.handle)
-
- def _get_power_limit(self) -> Union[int, None]:
- """Returns max power usage in milliwatts
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g263b5bf552d5ec7fcd29a088264d10ad
- """
- try:
- return pynvml.nvmlDeviceGetEnforcedPowerLimit(self.handle)
- except Exception:
- return None
+AMDGPUDevice = gpu_amd.AMDGPUDevice
+NvidiaGPUDevice = gpu_nvidia.NvidiaGPUDevice
+is_rocm_system = gpu_amd.is_rocm_system
+is_nvidia_system = gpu_nvidia.is_nvidia_system
- def _get_gpu_utilization(self):
- """Returns the % of utilization of the kernels during the last sample
- https://docs.nvidia.com/deploy/nvml-api/structnvmlUtilization__t.html#structnvmlUtilization__t
- """
- return pynvml.nvmlDeviceGetUtilizationRates(self.handle).gpu
-
- def _get_compute_mode(self) -> int:
- """Returns the compute mode of the GPU
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceEnumvs.html#group__nvmlDeviceEnumvs_1gbed1b88f2e3ba39070d31d1db4340233
- """
- return pynvml.nvmlDeviceGetComputeMode(self.handle)
-
- def _get_compute_processes(self) -> List:
- """Returns the list of processes ids having a compute context on the
- device with the memory used
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g46ceaea624d5c96e098e03c453419d68
- """
- try:
- processes = pynvml.nvmlDeviceGetComputeRunningProcesses(self.handle)
-
- return [{"pid": p.pid, "used_memory": p.usedGpuMemory} for p in processes]
- except pynvml.NVMLError:
- return []
-
- def _get_graphics_processes(self) -> List:
- """Returns the list of processes ids having a graphics context on the
- device with the memory used
- https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g7eacf7fa7ba4f4485d166736bf31195e
- """
- try:
- processes = pynvml.nvmlDeviceGetGraphicsRunningProcesses(self.handle)
-
- return [{"pid": p.pid, "used_memory": p.usedGpuMemory} for p in processes]
- except pynvml.NVMLError:
- return []
+# Backward-compatible module attributes
+amdsmi = gpu_amd.amdsmi
+pynvml = gpu_nvidia.pynvml
class AllGPUDevices:
+ device_count: int
+ devices: List[GPUDevice]
+
def __init__(self) -> None:
- if is_gpu_details_available():
+ gpu_details_available = is_gpu_details_available()
+ if gpu_details_available:
logger.debug("GPU available. Starting setup")
- self.device_count = pynvml.nvmlDeviceGetCount()
else:
logger.error("There is no GPU available")
- self.device_count = 0
self.devices = []
- for i in range(self.device_count):
- handle = pynvml.nvmlDeviceGetHandleByIndex(i)
- gpu_device = GPUDevice(handle=handle, gpu_index=i)
- self.devices.append(gpu_device)
+
+ if PYNVML_AVAILABLE:
+ logger.debug("PyNVML available. Starting setup")
+ gpu_nvidia.pynvml.nvmlInit()
+ nvidia_devices_count = gpu_nvidia.pynvml.nvmlDeviceGetCount()
+ for i in range(nvidia_devices_count):
+ handle = gpu_nvidia.pynvml.nvmlDeviceGetHandleByIndex(i)
+ nvidia_gpu_device = NvidiaGPUDevice(handle=handle, gpu_index=i)
+ self.devices.append(nvidia_gpu_device)
+
+ if AMDSMI_AVAILABLE:
+ logger.debug("AMDSMI available. Starting setup")
+ try:
+ gpu_amd.amdsmi.amdsmi_init()
+ amd_devices_handles = gpu_amd.amdsmi.amdsmi_get_processor_handles()
+ if len(amd_devices_handles) == 0:
+ logger.warning(
+ "No AMD GPUs found on machine with amdsmi_get_processor_handles() !"
+ )
+ else:
+ for i, handle in enumerate(amd_devices_handles):
+ # Try to get the actual device index from BDF (Bus/Device/Function)
+ # If this fails, fall back to enumeration index
+ try:
+ bdf_info = gpu_amd.amdsmi.amdsmi_get_gpu_device_bdf(handle)
+ # BDF typically contains domain, bus, device, function
+ # The device portion often corresponds to the GPU index
+ # For now, we'll use the enumeration index but log the BDF
+ logger.debug(
+ f"Found AMD GPU device with handle {handle}, enum_index {i}, BDF {bdf_info}: {gpu_amd.amdsmi.amdsmi_get_gpu_device_uuid(handle)}"
+ )
+ # Use enumerate index for now - this will be the index in the filtered list
+ gpu_index = i
+ except Exception:
+ logger.debug(
+ f"Found AMD GPU device with handle {handle} and index {i} : {gpu_amd.amdsmi.amdsmi_get_gpu_device_uuid(handle)}"
+ )
+ gpu_index = i
+
+ amd_gpu_device = AMDGPUDevice(
+ handle=handle, gpu_index=gpu_index
+ )
+ self.devices.append(amd_gpu_device)
+ except gpu_amd.amdsmi.AmdSmiException as e:
+ logger.warning(f"Failed to initialize AMDSMI: {e}", exc_info=True)
+ self.device_count = len(self.devices)
+
+ def start(self) -> None:
+ for device in self.devices:
+ if hasattr(device, "start"):
+ device.start()
def get_gpu_static_info(self) -> List:
"""Get all GPUs static information.
@@ -239,7 +101,7 @@ def get_gpu_static_info(self) -> List:
devices_static_info.append(gpu_device.get_static_details())
return devices_static_info
- except pynvml.NVMLError:
+ except Exception:
logger.warning("Failed to retrieve gpu static info", exc_info=True)
return []
@@ -267,11 +129,11 @@ def get_gpu_details(self) -> List:
try:
devices_info = []
for i in range(self.device_count):
- gpu_device: GPUDevice = self.devices[i]
+ gpu_device = self.devices[i]
devices_info.append(gpu_device.get_gpu_details())
return devices_info
- except pynvml.NVMLError:
+ except Exception:
logger.warning("Failed to retrieve gpu information", exc_info=True)
return []
@@ -290,20 +152,15 @@ def get_delta(self, last_duration: Time) -> List:
try:
devices_info = []
for i in range(self.device_count):
- gpu_device: GPUDevice = self.devices[i]
+ gpu_device = self.devices[i]
devices_info.append(gpu_device.delta(last_duration))
return devices_info
- except pynvml.NVMLError:
+ except Exception:
logger.warning("Failed to retrieve gpu information", exc_info=True)
return []
def is_gpu_details_available() -> bool:
"""Returns True if the GPU details are available."""
- try:
- pynvml.nvmlInit()
- return True
-
- except pynvml.NVMLError:
- return False
+ return PYNVML_AVAILABLE or AMDSMI_AVAILABLE
diff --git a/codecarbon/core/gpu_amd.py b/codecarbon/core/gpu_amd.py
new file mode 100644
index 000000000..6cff5a287
--- /dev/null
+++ b/codecarbon/core/gpu_amd.py
@@ -0,0 +1,272 @@
+import subprocess
+from collections import namedtuple
+from typing import Callable
+
+from codecarbon.core.gpu_device import GPUDevice
+from codecarbon.external.logger import logger
+
+
+def is_rocm_system():
+ """Returns True if the system has an rocm-smi interface."""
+ try:
+ # Check if rocm-smi is available
+ subprocess.check_output(["rocm-smi", "--help"])
+ return True
+ except (subprocess.CalledProcessError, OSError):
+ return False
+
+
+try:
+ import amdsmi
+
+ AMDSMI_AVAILABLE = True
+except ImportError:
+ amdsmi = None
+ if is_rocm_system():
+ logger.warning(
+ "AMD GPU detected but amdsmi is not available. "
+ "Please install amdsmi to get GPU metrics."
+ )
+ AMDSMI_AVAILABLE = False
+except AttributeError as e:
+ amdsmi = None
+ # In some environments, amdsmi may be present but not properly configured, leading to AttributeError when importing
+ logger.warning(
+ "AMD GPU detected but amdsmi is not properly configured. "
+ "Please ensure amdsmi is correctly installed to get GPU metrics."
+ "Tips : check consistency between Python amdsmi package and ROCm versions, and ensure AMD drivers are up to date."
+ f" Error: {e}"
+ )
+ AMDSMI_AVAILABLE = False
+
+
+class AMDGPUDevice(GPUDevice):
+ _dual_gcd_warning_emitted = False
+
+ def _is_dual_gcd_power_limited_model(self, gpu_name: str) -> bool:
+ name = gpu_name.upper()
+ # Dual-GCD models: MI2xx (except MI210) and MI3xx series
+ if "MI210" in name:
+ return False
+ return "MI2" in name or "MI3" in name
+
+ def _init_static_details(self) -> None:
+ super()._init_static_details()
+
+ self._known_zero_energy_counter = self._is_dual_gcd_power_limited_model(
+ self._gpu_name
+ )
+
+ def emit_selection_warning(self) -> None:
+ if not self._known_zero_energy_counter:
+ return
+
+ if not self.__class__._dual_gcd_warning_emitted:
+ logger.warning(
+ "Detected AMD Instinct MI250/MI250X/MI300X/MI300A family GPU. "
+ "These dual-GCD devices report power on one GCD while the other reports zero."
+ )
+ self.__class__._dual_gcd_warning_emitted = True
+
+ if self.gpu_index % 2 == 1:
+ logger.warning(
+ f"GPU {self._gpu_name} with index {self.gpu_index} is expected to report zero energy consumption due to being the second GCD in a dual-GCD configuration."
+ )
+ else:
+ logger.warning(
+ f"GPU {self._gpu_name} with index {self.gpu_index} is expected to report both GCDs' energy consumption as it is the first GCD in a dual-GCD configuration."
+ )
+
+ def _is_amdsmi_not_initialized_error(self, error: Exception) -> bool:
+ ret_code = getattr(error, "ret_code", None)
+ if ret_code == 32:
+ return True
+ error_message = str(error)
+ return "AMDSMI_STATUS_NOT_INIT" in error_message or "| 32 |" in error_message
+
+ def _call_amdsmi_with_reinit(self, func: Callable, *args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except amdsmi.amdsmi_exception.AmdSmiLibraryException as error:
+ if not self._is_amdsmi_not_initialized_error(error):
+ raise
+
+ logger.warning(
+ "AMDSMI reported device not initialized. Reinitializing and retrying once.",
+ exc_info=True,
+ )
+ amdsmi.amdsmi_init()
+ return func(*args, **kwargs)
+
+ def _get_gpu_metrics_info(self):
+ """Helper function to get all GPU metrics at once, to minimize the number of calls to amdsmi and reduce the risk of hitting not initialized error"""
+ return self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_metrics_info, self.handle
+ )
+
+ def _get_total_energy_consumption(self):
+ """Returns energy in millijoules.
+ amdsmi_get_energy_count returns accumulated energy counter and its resolution.
+ Energy = counter_value * counter_resolution (in µJ), convert to mJ.
+ """
+ try:
+ energy_count = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_energy_count, self.handle
+ )
+ energy_key = None
+ if "energy_accumulator" in energy_count:
+ energy_key = "energy_accumulator"
+ elif "power" in energy_count:
+ energy_key = "power"
+ if energy_key is None:
+ logger.warning(
+ f"Neither 'energy_accumulator' nor 'power' found in energy_count: {energy_count}"
+ )
+ return None
+ # The amdsmi library returns a dict with energy counter and resolution
+ # The counter is the actual accumulated value, resolution tells us how much each unit is worth
+ counter_value = energy_count.get(energy_key, 0)
+ counter_resolution_uj = energy_count.get("counter_resolution", 0)
+ if counter_value == 0 and counter_resolution_uj > 0:
+ # In some cases, the energy_accumulator is 0 but it exist in the metrics info, try to get it from there as a fallback
+ metrics_info = self._get_gpu_metrics_info()
+ counter_value = metrics_info.get(energy_key, 0)
+ if counter_value == 0:
+ if getattr(self, "_known_zero_energy_counter", False):
+ return 0
+ return None
+
+ # energy_in_µJ = counter_value * resolution_in_µJ
+ # Divide by 1000 to convert µJ to mJ
+ energy_mj = counter_value * counter_resolution_uj / 1000
+ return energy_mj
+ except Exception:
+ logger.warning(
+ "Failed to retrieve AMD GPU total energy consumption", exc_info=True
+ )
+ return None
+
+ def _get_gpu_name(self):
+ """Returns the name of the GPU device"""
+ try:
+ asic_info = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_asic_info, self.handle
+ )
+ name = asic_info.get("market_name", "Unknown GPU")
+ except Exception:
+ name = "Unknown GPU"
+ return self._to_utf8(name)
+
+ def _get_uuid(self):
+ """Returns the globally unique GPU device UUID"""
+ uuid = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_device_uuid, self.handle
+ )
+ return self._to_utf8(uuid)
+
+ def _get_memory_info(self):
+ """Returns memory info in bytes"""
+ memory_info = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_vram_usage, self.handle
+ )
+ AMDMemory = namedtuple("AMDMemory", ["total", "used", "free"])
+ # vram_total and vram_used are already in MB
+ total_mb = memory_info["vram_total"]
+ used_mb = memory_info["vram_used"]
+ return AMDMemory(
+ total=total_mb * 1024 * 1024,
+ used=used_mb * 1024 * 1024,
+ free=(total_mb - used_mb) * 1024 * 1024,
+ )
+
+ def _get_temperature(self):
+ """Returns degrees in the Celsius scale. Returns temperature in millidegrees Celsius."""
+ try:
+ # amdsmi_get_temp_metric returns temperature in millidegrees Celsius
+ temp_milli_celsius = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_temp_metric,
+ self.handle,
+ sensor_type=amdsmi.AmdSmiTemperatureType.HOTSPOT,
+ metric=amdsmi.AmdSmiTemperatureMetric.CURRENT,
+ )
+ # Convert from millidegrees to degrees
+ temp = temp_milli_celsius // 1000
+ # In some cases, the hotspot temperature can be 0 or not available, try to get it from metrics info as a fallback
+ if temp == 0:
+ metrics_info = self._get_gpu_metrics_info()
+ temp_celsius = metrics_info.get("temperature_hotspot", 0)
+ temp = temp_celsius
+ except amdsmi.amdsmi_exception.AmdSmiLibraryException as e:
+ logger.debug(f"Failed to retrieve gpu temperature: {e}")
+ temp = 0
+
+ return temp
+
+ def _get_power_usage(self):
+ """Returns power usage in Watts"""
+ power_info = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_power_info, self.handle
+ )
+
+ try:
+ power = int(power_info.get("average_socket_power", 0))
+ except (ValueError, TypeError):
+ power = 0
+
+ if power == 0:
+ # In some cases, the average_socket_power can be 0 or not available, try to get it from metrics info as a fallback
+ try:
+ metrics_info = self._get_gpu_metrics_info()
+ power = int(metrics_info.get("average_socket_power", 0))
+ except (ValueError, TypeError):
+ power = 0
+
+ return power
+
+ def _get_power_limit(self):
+ """Returns max power usage in Watts"""
+ # Get power cap info which contains power_cap in uW (microwatts)
+ try:
+ power_cap_info = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_power_cap_info, self.handle
+ )
+ # power_cap is in uW, convert to W
+ return int(power_cap_info["power_cap"] / 1_000_000)
+ except Exception:
+ logger.warning("Failed to retrieve gpu power cap", exc_info=True)
+ return None
+
+ def _get_gpu_utilization(self):
+ """Returns the % of utilization of the kernels during the last sample"""
+ activity = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_activity, self.handle
+ )
+ return activity["gfx_activity"]
+
+ def _get_compute_mode(self):
+ """Returns the compute mode of the GPU"""
+ return None
+
+ def _get_compute_processes(self):
+ """Returns the list of processes ids having a compute context on the device with the memory used"""
+ try:
+ processes = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_process_list, self.handle
+ )
+ return [{"pid": p["pid"], "used_memory": p["mem"]} for p in processes]
+ except Exception:
+ return []
+
+ def _get_graphics_processes(self):
+ """Returns the list of processes ids having a graphics context on the device with the memory used"""
+ try:
+ processes = self._call_amdsmi_with_reinit(
+ amdsmi.amdsmi_get_gpu_process_list, self.handle
+ )
+ return [
+ {"pid": p["pid"], "used_memory": p["mem"]}
+ for p in processes
+ if p["engine_usage"].get("gfx", 0) > 0
+ ]
+ except Exception:
+ return []
diff --git a/codecarbon/core/gpu_device.py b/codecarbon/core/gpu_device.py
new file mode 100644
index 000000000..4d7261b7d
--- /dev/null
+++ b/codecarbon/core/gpu_device.py
@@ -0,0 +1,116 @@
+from dataclasses import dataclass, field
+from typing import Any, Dict
+
+from codecarbon.core.units import Energy, Power, Time
+
+
+@dataclass
+class GPUDevice:
+ """
+ Represents a GPU device with associated energy and power metrics.
+
+ Attributes:
+ handle (any): An identifier for the GPU device.
+ gpu_index (int): The index of the GPU device in the system.
+ energy_delta (Energy): The amount of energy consumed by the GPU device
+ since the last measurement, expressed in kilowatt-hours (kWh).
+ Defaults to an initial value of 0 kWh.
+ power (Power): The current power consumption of the GPU device,
+ measured in watts (W). Defaults to an initial value of 0 W.
+ last_energy (Energy): The last recorded energy reading for the GPU
+ device, expressed in kilowatt-hours (kWh). This is used to
+ calculate `energy_delta`. Defaults to an initial value of 0 kWh.
+ """
+
+ handle: any
+ gpu_index: int
+ # Power based on reading
+ power: Power = field(default_factory=lambda: Power(0))
+ # Energy consumed in kWh
+ energy_delta: Energy = field(default_factory=lambda: Energy(0))
+ # Last energy reading in kWh
+ last_energy: Energy = field(default_factory=lambda: Energy(0))
+
+ def start(self) -> None:
+ self.last_energy = self._get_energy_kwh()
+
+ def __post_init__(self) -> None:
+ self.last_energy = self._get_energy_kwh()
+ self._init_static_details()
+
+ def _get_energy_kwh(self) -> Energy:
+ total_energy_consumption = self._get_total_energy_consumption()
+ if total_energy_consumption is None:
+ return self.last_energy
+ return Energy.from_millijoules(total_energy_consumption)
+
+ def delta(self, duration: Time) -> dict:
+ """
+ Compute the energy/power used since last call.
+ """
+ new_last_energy = energy = self._get_energy_kwh()
+ self.power = self.power.from_energies_and_delay(
+ energy, self.last_energy, duration
+ )
+ self.energy_delta = energy - self.last_energy
+ self.last_energy = new_last_energy
+ return {
+ "name": self._gpu_name,
+ "uuid": self._uuid,
+ "gpu_index": self.gpu_index,
+ "delta_energy_consumption": self.energy_delta,
+ "power_usage": self.power,
+ }
+
+ def get_static_details(self) -> Dict[str, Any]:
+ return {
+ "name": self._gpu_name,
+ "uuid": self._uuid,
+ "total_memory": self._total_memory,
+ "power_limit": self._power_limit,
+ "gpu_index": self.gpu_index,
+ }
+
+ def _init_static_details(self) -> None:
+ self._gpu_name = self._get_gpu_name()
+ self._uuid = self._get_uuid()
+ self._power_limit = self._get_power_limit()
+ # Get the memory
+ memory = self._get_memory_info()
+ self._total_memory = memory.total
+
+ def get_gpu_details(self) -> Dict[str, Any]:
+ # Memory
+ memory = self._get_memory_info()
+
+ device_details = {
+ "name": self._gpu_name,
+ "uuid": self._uuid,
+ "gpu_index": self.gpu_index,
+ "free_memory": memory.free,
+ "total_memory": memory.total,
+ "used_memory": memory.used,
+ "temperature": self._get_temperature(),
+ "power_usage": self._get_power_usage(),
+ "power_limit": self._power_limit,
+ "total_energy_consumption": self._get_total_energy_consumption(),
+ "gpu_utilization": self._get_gpu_utilization(),
+ "compute_mode": self._get_compute_mode(),
+ "compute_processes": self._get_compute_processes(),
+ "graphics_processes": self._get_graphics_processes(),
+ }
+ return device_details
+
+ def _to_utf8(self, str_or_bytes) -> Any:
+ if hasattr(str_or_bytes, "decode"):
+ return str_or_bytes.decode("utf-8", errors="replace")
+
+ return str_or_bytes
+
+ def emit_selection_warning(self) -> None:
+ """Hook for backend-specific warnings when a GPU is explicitly selected.
+
+ Backends that need to emit warnings for selected devices should override
+ this method. The default implementation is intentionally a no-op.
+ """
+ return None
diff --git a/codecarbon/core/gpu_nvidia.py b/codecarbon/core/gpu_nvidia.py
new file mode 100644
index 000000000..ddda4c57d
--- /dev/null
+++ b/codecarbon/core/gpu_nvidia.py
@@ -0,0 +1,130 @@
+import subprocess
+from dataclasses import dataclass
+from typing import Any, Union
+
+from codecarbon.core.gpu_device import GPUDevice
+from codecarbon.external.logger import logger
+
+
+def is_nvidia_system():
+ """Returns True if the system has an nvidia-smi interface."""
+ try:
+ # Check if nvidia-smi is available
+ subprocess.check_output(["nvidia-smi", "--help"])
+ return True
+ except Exception:
+ return False
+
+
+try:
+ import pynvml
+
+ pynvml.nvmlInit()
+ PYNVML_AVAILABLE = True
+except ImportError:
+ pynvml = None
+ if is_nvidia_system():
+ logger.warning(
+ "Nvidia GPU detected but pynvml is not available. "
+ "Please install pynvml to get GPU metrics."
+ )
+ PYNVML_AVAILABLE = False
+except Exception:
+ pynvml = None
+ if is_nvidia_system():
+ logger.warning(
+ "Nvidia GPU detected but pynvml initialization failed. "
+ "Please ensure NVIDIA drivers are properly installed."
+ )
+ PYNVML_AVAILABLE = False
+
+
+@dataclass
+class NvidiaGPUDevice(GPUDevice):
+ def _get_total_energy_consumption(self) -> int:
+ """Returns total energy consumption for this GPU in millijoules (mJ) since the driver was last reloaded
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g732ab899b5bd18ac4bfb93c02de4900a
+ """
+ try:
+ return pynvml.nvmlDeviceGetTotalEnergyConsumption(self.handle)
+ except pynvml.NVMLError:
+ logger.warning(
+ "Failed to retrieve gpu total energy consumption", exc_info=True
+ )
+ return None
+
+ def _get_gpu_name(self) -> Any:
+ """Returns the name of the GPU device
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1ga5361803e044c6fdf3b08523fb6d1481
+ """
+ try:
+ name = pynvml.nvmlDeviceGetName(self.handle)
+ return self._to_utf8(name)
+ except UnicodeDecodeError:
+ return "Unknown GPU"
+
+ def _get_uuid(self):
+ """Returns the globally unique GPU device UUID
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g72710fb20f30f0c2725ce31579832654
+ """
+ uuid = pynvml.nvmlDeviceGetUUID(self.handle)
+ return self._to_utf8(uuid)
+
+ def _get_memory_info(self):
+ """Returns memory info in bytes
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g2dfeb1db82aa1de91aa6edf941c85ca8
+ """
+ try:
+ return pynvml.nvmlDeviceGetMemoryInfo(self.handle)
+ except pynvml.NVMLError_NotSupported:
+ # error thrown for the NVIDIA Blackwell GPU of DGX Spark, due to memory sharing -> return defaults instead
+ return pynvml.c_nvmlMemory_t(-1, -1, -1)
+
+ def _get_temperature(self) -> int:
+ """Returns degrees in the Celsius scale
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g92d1c5182a14dd4be7090e3c1480b121
+ """
+ return pynvml.nvmlDeviceGetTemperature(self.handle, pynvml.NVML_TEMPERATURE_GPU)
+
+ def _get_power_usage(self) -> int:
+ """Returns power usage in Watts
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g7ef7dff0ff14238d08a19ad7fb23fc87
+ """
+ return pynvml.nvmlDeviceGetPowerUsage(self.handle) / 1000
+
+ def _get_power_limit(self) -> Union[int, None]:
+ """Returns max power usage in Watts
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g263b5bf552d5ec7fcd29a088264d10ad
+ """
+ try:
+ # convert from milliwatts to watts
+ return pynvml.nvmlDeviceGetEnforcedPowerLimit(self.handle) / 1000
+ except Exception:
+ logger.warning("Failed to retrieve gpu power limit", exc_info=True)
+ return None
+
+ def _get_gpu_utilization(self):
+ """Returns the % of utilization of the kernels during the last sample
+ https://docs.nvidia.com/deploy/nvml-api/structnvmlUtilization__t.html#structnvmlUtilization__t
+ """
+ return pynvml.nvmlDeviceGetUtilizationRates(self.handle).gpu
+
+ def _get_compute_mode(self) -> int:
+ """Returns the compute mode of the GPU
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceEnumvs.html#group__nvmlDeviceEnumvs_1gbed1b88f2e3ba39070d31d1db4340233
+ """
+ return pynvml.nvmlDeviceGetComputeMode(self.handle)
+
+ def _get_compute_processes(self):
+ """Returns the list of processes ids having a compute context on the device with the memory used
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g46ceaea624d5c96e098e03c453419d68
+ """
+ processes = pynvml.nvmlDeviceGetComputeRunningProcesses(self.handle)
+ return [{"pid": p.pid, "used_memory": p.usedGpuMemory} for p in processes]
+
+ def _get_graphics_processes(self):
+ """Returns the list of processes ids having a graphics context on the device with the memory used
+ https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1g7eacf7fa7ba4f4485d166736bf31195e
+ """
+ processes = pynvml.nvmlDeviceGetGraphicsRunningProcesses(self.handle)
+ return [{"pid": p.pid, "used_memory": p.usedGpuMemory} for p in processes]
diff --git a/codecarbon/core/measure.py b/codecarbon/core/measure.py
index 6f612a5da..d11885ed6 100644
--- a/codecarbon/core/measure.py
+++ b/codecarbon/core/measure.py
@@ -1,3 +1,7 @@
+"""
+TODO: This look like this class is not used yet, but it will be nice to use it the future for readability of codecarbon/emissions_tracker.py
+"""
+
from time import perf_counter
from codecarbon.external.hardware import CPU, GPU, RAM, AppleSiliconChip
@@ -60,7 +64,7 @@ def do_measure(self) -> None:
self._total_gpu_energy += energy
self._gpu_power = power
logger.info(
- f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f"do_measure() Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f". Total GPU Power : {self._gpu_power.W} W"
)
elif isinstance(hardware, RAM):
diff --git a/codecarbon/core/resource_tracker.py b/codecarbon/core/resource_tracker.py
index adef7b947..67786189d 100644
--- a/codecarbon/core/resource_tracker.py
+++ b/codecarbon/core/resource_tracker.py
@@ -2,7 +2,7 @@
from typing import List, Union
from codecarbon.core import cpu, gpu, powermetrics
-from codecarbon.core.config import parse_gpu_ids
+from codecarbon.core.config import normalize_gpu_ids
from codecarbon.core.util import (
detect_cpu_model,
is_linux_os,
@@ -221,14 +221,20 @@ def set_CPU_tracking(self):
def set_GPU_tracking(self):
logger.info("[setup] GPU Tracking...")
- if self.tracker._gpu_ids:
- self.tracker._gpu_ids = parse_gpu_ids(self.tracker._gpu_ids)
- if self.tracker._gpu_ids:
- self.tracker._conf["gpu_ids"] = self.tracker._gpu_ids
- self.tracker._conf["gpu_count"] = len(self.tracker._gpu_ids)
+ self.tracker._gpu_ids = normalize_gpu_ids(self.tracker._gpu_ids)
+ self.tracker._conf["gpu_ids"] = self.tracker._gpu_ids
+ if self.tracker._gpu_ids is not None:
+ self.tracker._conf["gpu_count"] = len(self.tracker._gpu_ids)
- if gpu.is_gpu_details_available():
- logger.info("Tracking Nvidia GPU via pynvml")
+ is_nvidia = gpu.is_nvidia_system()
+ is_rocm = gpu.is_rocm_system()
+ if is_nvidia or is_rocm:
+ if is_nvidia:
+ logger.info("Tracking Nvidia GPUs via PyNVML")
+ self.gpu_tracker = "pynvml"
+ else:
+ logger.info("Tracking AMD GPUs via AMDSMI")
+ self.gpu_tracker = "amdsmi"
gpu_devices = GPU.from_utils(self.tracker._gpu_ids)
self.tracker._hardware.append(gpu_devices)
gpu_names = [n["name"] for n in gpu_devices.devices.get_gpu_static_info()]
@@ -236,11 +242,9 @@ def set_GPU_tracking(self):
self.tracker._conf["gpu_model"] = "".join(
[f"{i} x {name}" for name, i in gpu_names_dict.items()]
)
- if self.tracker._conf.get("gpu_count") is None:
- self.tracker._conf["gpu_count"] = len(
- gpu_devices.devices.get_gpu_static_info()
- )
- self.gpu_tracker = "pynvml"
+ self.tracker._conf["gpu_count"] = len(
+ gpu_devices.devices.get_gpu_static_info()
+ )
else:
logger.info("No GPU found.")
self.tracker._conf.setdefault("gpu_count", 0)
diff --git a/codecarbon/core/util.py b/codecarbon/core/util.py
index da13dd301..744b2e3e5 100644
--- a/codecarbon/core/util.py
+++ b/codecarbon/core/util.py
@@ -151,15 +151,15 @@ def count_cpus() -> int:
try:
logger.debug(
- "SLURM environment detected for job {SLURM_JOB_ID}, running"
- + " `scontrol show job $SLURM_JOB_ID` to count SLURM-available cpus."
+ f"SLURM environment detected for job {SLURM_JOB_ID}, running"
+ + f" `scontrol show job {SLURM_JOB_ID}` to count SLURM-available cpus."
)
scontrol = subprocess.check_output(
[f"scontrol show job {SLURM_JOB_ID}"], shell=True
).decode()
except subprocess.CalledProcessError:
logger.warning(
- "Error running `scontrol show job $SLURM_JOB_ID` "
+ f"Error running `scontrol show job {SLURM_JOB_ID}` "
+ "to count SLURM-available cpus. Using the machine's cpu count."
)
return psutil.cpu_count(logical=True)
@@ -168,18 +168,24 @@ def count_cpus() -> int:
if len(num_cpus_matches) == 0:
logger.warning(
- "Could not find NumCPUs= after running `scontrol show job $SLURM_JOB_ID` "
+ f"Could not find NumCPUs= after running `scontrol show job {SLURM_JOB_ID}` "
+ "to count SLURM-available cpus. Using the machine's cpu count."
)
return psutil.cpu_count(logical=True)
if len(num_cpus_matches) > 1:
logger.warning(
- "Unexpected output after running `scontrol show job $SLURM_JOB_ID` "
+ f"Unexpected output after running `scontrol show job {SLURM_JOB_ID}` "
+ "to count SLURM-available cpus. Using the machine's cpu count."
)
return psutil.cpu_count(logical=True)
num_cpus = num_cpus_matches[0].replace("NumCPUs=", "")
logger.debug(f"Detected {num_cpus} cpus available on SLURM.")
+
+ num_gpus_matches = re.findall(r"gres/gpu=\d+", scontrol)
+ if len(num_gpus_matches) > 0:
+ num_gpus = num_gpus_matches[0].replace("gres/gpu=", "")
+ logger.debug(f"Detected {num_gpus} gpus available on SLURM.")
+
return int(num_cpus)
diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py
index a070ea56c..57c9786ae 100644
--- a/codecarbon/emissions_tracker.py
+++ b/codecarbon/emissions_tracker.py
@@ -17,7 +17,7 @@
import psutil
from codecarbon._version import __version__
-from codecarbon.core.config import get_hierarchical_config
+from codecarbon.core.config import get_hierarchical_config, normalize_gpu_ids
from codecarbon.core.emissions import Emissions
from codecarbon.core.resource_tracker import ResourceTracker
from codecarbon.core.units import Energy, Power, Time, Water
@@ -143,8 +143,17 @@ def _set_from_conf(
if not os.path.exists(value):
raise OSError(f"Folder '{value}' doesn't exist !")
if name == "gpu_ids":
+ logger.debug(
+ f"CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES')}"
+ )
+ logger.debug(
+ f"ROCR_VISIBLE_DEVICES: {os.environ.get('ROCR_VISIBLE_DEVICES')}"
+ )
if value is None and os.environ.get("CUDA_VISIBLE_DEVICES"):
value = os.environ.get("CUDA_VISIBLE_DEVICES")
+ elif value is None and os.environ.get("ROCR_VISIBLE_DEVICES"):
+ value = os.environ.get("ROCR_VISIBLE_DEVICES")
+ value = normalize_gpu_ids(value)
# store final value
self._conf[name] = value
# set `self._{name}` to `value`
@@ -383,7 +392,6 @@ def __init__(
self._tasks: Dict[str, Task] = {}
self._active_task: Optional[str] = None
self._active_task_emissions_at_start: Optional[EmissionsData] = None
-
# Tracking mode detection
self._hardware = []
resource_tracker = ResourceTracker(self)
@@ -968,9 +976,13 @@ def _monitor_power(self) -> None:
# Collect GPU utilization metrics
for hardware in self._hardware:
if isinstance(hardware, GPU):
+ gpu_ids_to_monitor = hardware.gpu_ids
gpu_details = hardware.devices.get_gpu_details()
for gpu_detail in gpu_details:
- if "gpu_utilization" in gpu_detail:
+ if (
+ gpu_detail["gpu_index"] in gpu_ids_to_monitor
+ and "gpu_utilization" in gpu_detail
+ ):
self._gpu_utilization_history.append(
gpu_detail["gpu_utilization"]
)
@@ -1010,6 +1022,7 @@ def _do_measurements(self) -> None:
f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f". Total GPU Power : {self._gpu_power.W} W"
)
+
elif isinstance(hardware, RAM):
self._total_ram_energy += energy
self._ram_power = power
@@ -1035,7 +1048,7 @@ def _do_measurements(self) -> None:
# Accumulate for running average
self._gpu_power_sum += power.W
logger.info(
- f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f"Energy consumed for all AppleSilicon GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f". Total GPU Power : {self._gpu_power.W} W"
)
else:
diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py
index aaa548617..c2369a5f5 100644
--- a/codecarbon/external/hardware.py
+++ b/codecarbon/external/hardware.py
@@ -68,6 +68,11 @@ def __post_init__(self):
self._total_power = Power(
0 # It will be 0 until we call for the first time measure_power_and_energy
)
+ self._gpu_ids_resolved = False
+
+ def start(self) -> None:
+ if hasattr(self.devices, "start"):
+ self.devices.start()
def measure_power_and_energy(
self, last_duration: float, gpu_ids: Iterable[int] = None
@@ -82,8 +87,8 @@ def measure_power_and_energy(
sum(
[
gpu_details["delta_energy_consumption"].kWh
- for idx, gpu_details in enumerate(all_gpu_details)
- if idx in gpu_ids
+ for gpu_details in all_gpu_details
+ if gpu_details["gpu_index"] in gpu_ids
]
)
)
@@ -91,8 +96,8 @@ def measure_power_and_energy(
sum(
[
gpu_details["power_usage"].kW
- for idx, gpu_details in enumerate(all_gpu_details)
- if idx in gpu_ids
+ for gpu_details in all_gpu_details
+ if gpu_details["gpu_index"] in gpu_ids
]
)
)
@@ -103,6 +108,11 @@ def _get_gpu_ids(self) -> Iterable[int]:
Get the Ids of the GPUs that we will monitor
:return: list of ids
"""
+ if getattr(self, "_gpu_ids_resolved", False):
+ return (
+ self.gpu_ids if self.gpu_ids is not None else list(range(self.num_gpus))
+ )
+
if self.gpu_ids is not None:
uuids_to_ids = {
gpu.get("uuid"): gpu.get("gpu_index")
@@ -111,13 +121,19 @@ def _get_gpu_ids(self) -> Iterable[int]:
monitored_gpu_ids = []
for gpu_id in self.gpu_ids:
+ logger.debug(f"Processing GPU ID: '{gpu_id}' (type: {type(gpu_id)})")
found_gpu_id = False
# Does it look like an index into the number of GPUs on the system?
if isinstance(gpu_id, int) or gpu_id.isdigit():
gpu_id = int(gpu_id)
if 0 <= gpu_id < self.num_gpus:
monitored_gpu_ids.append(gpu_id)
+ self._emit_selection_warning_for_gpu_id(gpu_id)
found_gpu_id = True
+ else:
+ logger.warning(
+ f"GPU ID {gpu_id} out of range [0, {self.num_gpus})"
+ )
# Does it match a prefix of any UUID on the system after stripping any 'MIG-'
# id prefix per https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-environment-variables ?
else:
@@ -128,6 +144,7 @@ def _get_gpu_ids(self) -> Iterable[int]:
f"Matching GPU ID {stripped_gpu_id_str} (originally {gpu_id}) against {uuid} for GPU index {id}"
)
monitored_gpu_ids.append(id)
+ self._emit_selection_warning_for_gpu_id(id)
found_gpu_id = True
break
if not found_gpu_id:
@@ -136,25 +153,32 @@ def _get_gpu_ids(self) -> Iterable[int]:
)
monitored_gpu_ids = sorted(list(set(monitored_gpu_ids)))
+ logger.info(
+ f"Monitoring GPUs with indices: {monitored_gpu_ids} out of {self.num_gpus} total GPUs"
+ )
self.gpu_ids = monitored_gpu_ids
+ self._gpu_ids_resolved = True
return monitored_gpu_ids
else:
+ self._gpu_ids_resolved = True
return list(range(self.num_gpus))
+ def _emit_selection_warning_for_gpu_id(self, gpu_id: int) -> None:
+ for device in self.devices.devices:
+ if device.gpu_index != gpu_id:
+ continue
+ device.emit_selection_warning()
+
def total_power(self) -> Power:
return self._total_power
- def start(self) -> None:
- for d in self.devices.devices:
- d.start()
-
@classmethod
def from_utils(cls, gpu_ids: Optional[List] = None) -> "GPU":
gpus = cls(gpu_ids=gpu_ids)
new_gpu_ids = gpus._get_gpu_ids()
if len(new_gpu_ids) < gpus.num_gpus:
logger.warning(
- f"You have {gpus.num_gpus} GPUs but we will monitor only {len(new_gpu_ids)} ({new_gpu_ids}) of them. Check your configuration."
+ f"You have {gpus.num_gpus} GPUs but we will monitor only {len(new_gpu_ids)} ({new_gpu_ids}) of them."
)
return cls(gpu_ids=new_gpu_ids)
diff --git a/docs/advanced/adastra.md b/docs/advanced/adastra.md
new file mode 100644
index 000000000..c0ce444c9
--- /dev/null
+++ b/docs/advanced/adastra.md
@@ -0,0 +1,194 @@
+# ROCm and PyTorch on SLURM SuperComputer
+
+This project was provided with computing and storage resources by GENCI at CINES thanks to the grant AD010615147R1 on the [supercomputer Adastra](https://dci.dci-gitlab.cines.fr/webextranet/architecture/index.html)'s MI250x/MI300 partition.
+
+Thanks to this grant we were able to develop and test the AMD ROCM support in CodeCarbon, and provide this quick start guide to help other users of Adastra HPC to easily monitor the carbon emissions of their machine learning workloads running on AMD GPUs.
+
+It was tested on Adastra but it will likely work on any SLURM cluster with AMD GPUs and ROCM support.
+
+## Quick Start Guide
+
+Adastra security rules require users to connect through a fixed IP. We choose to setup a small host in the cloud to act as a bastion server, allowing us to connect to Adastra from anywhere without needing to change our IP address.
+
+Adastra architecture is quite standard for a HPC cluster, with a login node and compute nodes. The login node has internet access and is the only one accessible from outside, while the compute nodes are where the GPU workloads run, without internet access.
+
+The Python environment is setup on the login node, and referenced by the compute nodes.
+
+The job is submitted from the login node using `sbatch`, and the SLURM script takes care of loading the Python environment and running the code on the compute node.
+
+If the `--time` option of `sbatch` is less than 30 minutes, the job will be put in the `debug` partition, which has a faster scheduling but a shorter maximum runtime.
+
+### Export your configuration
+
+Adapt the following environment variables with your own configuration. You can add them to your `.bashrc` or `.zshrc` for convenience.
+
+```bash
+export BASTION_IP="xx.xx.xx.xx"
+export BASTION_USER="username"
+export HPC_HOST="xx.xx.fr"
+export HPC_PASS="xxxxx"
+export PROJECT_ID="xxx"
+export USER_NAME="username_hpc"
+export HPC_PROJECT_FOLDER="/lus/home/xxx"
+```
+
+### Connect to CINES Adastra
+
+```bash
+sshpass -p "$HPC_PASS" ssh -J $BASTION_USER@$BASTION_IP $USER_NAME@$HPC_HOST
+```
+
+For the first time you may want to connect one-by-one to debug any SSH issue before using `sshpass`:
+
+```bash
+ssh -o ServerAliveInterval=60 $BASTION_USER@$BASTION_IP
+ssh -o ServerAliveInterval=60 $USER_NAME@$HPC_HOST
+```
+
+### Copy your code to Adastra
+
+```bash
+sshpass -p "$HPC_PASS" scp -r -J $BASTION_USER@$BASTION_IP /you/folder/* $USER_NAME@$HPC_HOST:$HPC_PROJECT_FOLDER
+```
+
+### Install CodeCarbon and dependencies
+
+Be careful to install the correct version of `amdsmi` that is compatible with the ROCM version on Adastra. The last available version we used is `7.0.1`.
+
+#### Simple installation
+
+
+```bash
+module load python/3.12
+module load rocm/7.0.1
+
+python -m venv .venv
+source .venv/bin/activate
+pip install --upgrade pip
+# Important: Adastra's MI250 runs ROCm 6.4.3 natively.
+# With export ROCM_PATH=/opt/rocm-6.4.3 in our SLURM script, this python wheel perfectly matches the C library without symlink issues!
+pip install amdsmi==7.0.1
+pip install codecarbon
+```
+
+#### use a branch of CodeCarbon with PyTorch
+
+```bash
+module load python/3.12
+module load rocm/7.0.1
+git clone https://github.com/mlco2/codecarbon.git
+# If you want a specific version, use git checkout to switch to the desired version.
+git checkout -b feat/rocm
+cd codecarbon
+python -m venv .venv
+source .venv/bin/activate
+python -V
+# Must be 3.12.x
+pip install --upgrade pip
+# Important: Adastra's MI250 runs ROCm 6.4.3 natively.
+# With export ROCM_PATH=/opt/rocm-6.4.3 in our SLURM script, this python wheel perfectly matches the C library without symlink issues!
+pip install amdsmi==7.0.1
+# Look at https://download.pytorch.org/whl/torch/ for the correct version matching your Python (cp312) and ROCM version.
+# torch-2.10.0+rocm7.0-cp312-cp312-manylinux_2_28_x86_64.whl
+pip3 install torch==2.10.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.0
+pip install numpy
+
+# Install CodeCarbon in editable mode to allow for live code changes without reinstallation
+pip install -e .
+```
+
+#### Development workflow
+
+You can code on the login Node, but we suggest to do the development on your local machine and then push the code to a repository (e.g., GitHub) and pull it from the login node. This way you avoid loosing code and keep tracks of the changes.
+
+After every connection to Adastra, you need to activate your Python environment:
+
+```bash
+cd codecarbon
+git pull
+source .venv/bin/activate
+```
+
+### Submit a Job
+
+**Option A: Using sbatch (recommended)**
+```bash
+sbatch examples/slurm_rocm/run_codecarbon_pytorch.slurm
+```
+
+### 4. Monitor Job Status
+```bash
+# View running jobs
+squeue -u $USER
+
+# View job output
+tail -f logs/.out
+```
+
+## Troubleshooting
+
+
+```
+Error :
+[codecarbon WARNING @ 10:28:46] AMD GPU detected but amdsmi is not properly configured. Please ensure amdsmi is correctly installed to get GPU metrics.Tips : check consistency between Python amdsmi package and ROCm versions, and ensure AMD drivers are up to date. Error: /opt/rocm/lib/libamd_smi.so: undefined symbol: amdsmi_get_cpu_affinity_with_scope
+```
+
+This mean you have a mismatch between the `amdsmi` Python package and the ROCM version installed on Adastra. To fix this, ensure you install the correct version of `amdsmi` that matches the ROCM version (e.g., `amdsmi==7.0.1` for ROCM 7.0.1).
+
+```bash
+KeyError: 'ROCM_PATH'
+```
+This means the rocm module is not loaded, load it with `module load rocm/7.0.1`.
+
+## Limitations and Future Work
+
+The AMD Instinct MI250 accelerator card contains two Graphics Compute Dies (GCDs) per physical card. However, when monitoring energy consumption (e.g., via rocm-smi or tools like CodeCarbon), only one GCD reports power usage, while the other shows zero values. This is problematic for accurate energy accounting, especially in HPC/SLURM environments where jobs may be allocated a single GCD.
+
+So in that case we display a warning.
+
+In a future work we will use `average_gfx_activity` to estimate the corresponding power of both GCDs, and provide an estimation instead of 0.
+
+## Documentation
+
+- [CINES Adastra GPU allocation](https://dci.dci-gitlab.cines.fr/webextranet/user_support/index.html#allocating-a-single-gpu)
+- [CINES PyTorch on ROCM](https://dci.dci-gitlab.cines.fr/webextranet/software_stack/libraries/index.html#pytorch)
+- [AMD SMI library](https://rocm.docs.amd.com/projects/amdsmi/en/latest/reference/amdsmi-py-api.html)
+
+
+## Annex: Example of Job Details with scontrol
+
+This trace was obtained to adapt `codecarbon/core/util.py` to properly parse the SLURM job details and extract the relevant information about GPU and CPU allocation.
+
+```
+[$PROJECT_ID] $USER_NAME@login5:~/codecarbon$ scontrol show job 4687018
+JobId=4687018 JobName=codecarbon-test
+ UserId=$USER_NAME(xxx) GroupId=grp_$USER_NAME(xxx) MCS_label=N/A
+ Priority=900000 Nice=0 Account=xxxxxx QOS=debug
+ JobState=COMPLETED Reason=None Dependency=(null)
+ Requeue=0 Restarts=0 BatchFlag=1 Reboot=0 ExitCode=0:0
+ RunTime=00:00:24 TimeLimit=00:05:00 TimeMin=N/A
+ SubmitTime=2026-03-02T17:12:49 EligibleTime=2026-03-02T17:12:49
+ AccrueTime=2026-03-02T17:12:49
+ StartTime=2026-03-02T17:12:49 EndTime=2026-03-02T17:13:13 Deadline=N/A
+ SuspendTime=None SecsPreSuspend=0 LastSchedEval=2026-03-02T17:12:49 Scheduler=Main
+ Partition=mi250-shared AllocNode:Sid=login5:2553535
+ ReqNodeList=(null) ExcNodeList=(null)
+ NodeList=g1341
+ BatchHost=g1341
+ NumNodes=1 NumCPUs=16 NumTasks=1 CPUs/Task=8 ReqB:S:C:T=0:0:*:1
+ ReqTRES=cpu=8,mem=29000M,node=1,billing=8,gres/gpu=1
+ AllocTRES=cpu=16,mem=29000M,energy=10211,node=1,billing=16,gres/gpu=1,gres/gpu:mi250x=1
+ Socks/Node=* NtasksPerN:B:S:C=1:0:*:1 CoreSpec=*
+ MinCPUsNode=8 MinMemoryNode=29000M MinTmpDiskNode=0
+ Features=MI250&DEBUG DelayBoot=00:00:00
+ OverSubscribe=OK Contiguous=0 Licenses=(null) Network=(null)
+ Command=/lus/home/CT6/$PROJECT_ID/$USER_NAME/codecarbon/run_codecarbon.sh
+ WorkDir=/lus/home/CT6/$PROJECT_ID/$USER_NAME/codecarbon
+ AdminComment=Accounting=1
+ StdErr=/lus/home/CT6/$PROJECT_ID/$USER_NAME/codecarbon/logs/4687018.err
+ StdIn=/dev/null
+ StdOut=/lus/home/CT6/$PROJECT_ID/$USER_NAME/codecarbon/logs/4687018.out
+ TresPerNode=gres/gpu:1
+ TresPerTask=cpu=8
+```
+
diff --git a/docs/advanced/ansible.md b/docs/advanced/ansible.md
new file mode 100644
index 000000000..b10592612
--- /dev/null
+++ b/docs/advanced/ansible.md
@@ -0,0 +1,65 @@
+# Deploy CodeCarbon CLI as a Service using Ansible
+
+This section describes how to deploy CodeCarbon as a system service
+using Ansible automation.
+
+It automate the manual installation done in the previous chapter.
+
+## What the Playbook Does
+
+The Ansible playbook automates the following tasks:
+
+- Creates a dedicated system user and group for CodeCarbon
+- Sets up a Python virtual environment
+- Installs CodeCarbon package
+- Configures RAPL permissions for power measurements
+- Creates and configures the systemd service
+- Sets up the CodeCarbon configuration file
+- Starts and enables the service
+
+## Prerequisites
+
+- Ansible installed on your machine
+- Debian-based target system(s)
+- SSH access to target system(s)
+- CodeCarbon API credentials from the dashboard
+
+## Directory Structure
+
+``` text
+codecarbon/deploy/ansible/codecarbon_cli_as_a_service/
+├── hosts
+├── tasks
+│ ├── install_codecarbon.yml
+│ ├── main.yml
+│ ├── rapl.yml
+│ └── systemd_service.yml
+├── templates
+│ ├── codecarbon.config.j2
+│ └── systemd_service.j2
+└── vars
+ └── main.yml
+```
+
+## Quick Start
+
+1. Set the the target to install in `hosts`:
+
+ ``` text
+ yourservername.yourdomain.com hostname=yourservername ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_ed25519
+ ```
+
+2. Update the variables in `vars/main.yml` with your configuration:
+
+ ``` yaml
+ organization_id: your_org_id
+ project_id: your_project_id
+ experiment_id: your_experiment_id
+ api_key: your_api_key
+ ```
+
+3. Run the playbook:
+
+ ``` bash
+ ansible-playbook -i hosts tasks/main.yml
+ ```
diff --git a/docs/getting-started/advanced_installation.md b/docs/advanced/linux_service.md
similarity index 57%
rename from docs/getting-started/advanced_installation.md
rename to docs/advanced/linux_service.md
index 1944c2834..8e8a9442c 100644
--- a/docs/getting-started/advanced_installation.md
+++ b/docs/advanced/linux_service.md
@@ -1,6 +1,4 @@
-# Advanced Installation
-
-## Install CodeCarbon as a Linux service
+# Install CodeCarbon as a Linux service
To install CodeCarbon as a Linux service, follow the instructions below.
It works on Ubuntu or other Debian-based systems using systemd.
@@ -112,70 +110,4 @@ journalctl -u codecarbon
You are done, CodeCarbon is now running as a service on your machine.
Wait 5 minutes for the first measure to be send to the dashboard at
-.
-
-## Deploy CodeCarbon CLI as a Service using Ansible
-
-This section describes how to deploy CodeCarbon as a system service
-using Ansible automation.
-
-It automate the manual installation done in the previous chapter.
-
-### What the Playbook Does
-
-The Ansible playbook automates the following tasks:
-
-- Creates a dedicated system user and group for CodeCarbon
-- Sets up a Python virtual environment
-- Installs CodeCarbon package
-- Configures RAPL permissions for power measurements
-- Creates and configures the systemd service
-- Sets up the CodeCarbon configuration file
-- Starts and enables the service
-
-### Prerequisites
-
-- Ansible installed on your machine
-- Debian-based target system(s)
-- SSH access to target system(s)
-- CodeCarbon API credentials from the dashboard
-
-### Directory Structure
-
-``` text
-codecarbon/deploy/ansible/codecarbon_cli_as_a_service/
-├── hosts
-├── tasks
-│ ├── install_codecarbon.yml
-│ ├── main.yml
-│ ├── rapl.yml
-│ └── systemd_service.yml
-├── templates
-│ ├── codecarbon.config.j2
-│ └── systemd_service.j2
-└── vars
- └── main.yml
-```
-
-### Quick Start
-
-1. Set the the target to install in `hosts`:
-
- ``` text
- yourservername.yourdomain.com hostname=yourservername ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_ed25519
- ```
-
-2. Update the variables in `vars/main.yml` with your configuration:
-
- ``` yaml
- organization_id: your_org_id
- project_id: your_project_id
- experiment_id: your_experiment_id
- api_key: your_api_key
- ```
-
-3. Run the playbook:
-
- ``` bash
- ansible-playbook -i hosts tasks/main.yml
- ```
+.
\ No newline at end of file
diff --git a/docs/getting-started/parameters.md b/docs/getting-started/parameters.md
index 7205a6509..67cd41458 100644
--- a/docs/getting-started/parameters.md
+++ b/docs/getting-started/parameters.md
@@ -9,7 +9,7 @@ Parameters can be set via `EmissionsTracker()`, `OfflineEmissionsTracker()`, the
up to 2.2, new greener ones as low as 1.1.
!!! note "GPU selection"
- If you use `CUDA_VISIBLE_DEVICES` to set GPUs, CodeCarbon will automatically
+ If you use `CUDA_VISIBLE_DEVICES` or `ROCR_VISIBLE_DEVICES` to set GPUs, CodeCarbon will automatically
populate `gpu_ids`. Manual `gpu_ids` overrides this.
## EmissionsTracker / BaseEmissionsTracker
diff --git a/docs/introduction/power_estimation.md b/docs/introduction/power_estimation.md
new file mode 100644
index 000000000..b56880429
--- /dev/null
+++ b/docs/introduction/power_estimation.md
@@ -0,0 +1,78 @@
+# How Power Estimation Works in CodeCarbon
+
+CodeCarbon tracks energy consumption by periodically querying the underlying hardware interfaces (e.g., RAPL for Intel CPUs, NVML for NVIDIA GPUs, AMDSMI for AMD GPUs) or by falling back on constant power models for non-supported hardware (such as generic CPU or RAM matching).
+
+While energy is the metric primarily responsible for CO₂ emissions estimations, tracking **power** (measured in Watts or kiloWatts) is equally important to provide meaningful dashboards and to help users understand their instantaneous consumption.
+
+## 1. Energy as the Source of Truth
+
+The most accurate tracking methods rely on built-in hardware energy counters rather than instantaneous power draw. For example:
+
+- **NVIDIA GPUs** using `nvmlDeviceGetTotalEnergyConsumption` return accumulated energy in millijoules.
+- **AMD GPUs** using `amdsmi_get_energy_count` yield a counter that is multiplied by its resolution and converted into millijoules.
+- **CPUs** using the RAPL interface read from files like `energy_uj` to get accumulated microjoules.
+- **RAM** using the RAPL interface read from files like `energy_uj` to get accumulated microjoules. See `rapl_include_dram` option. Not used by default.
+
+At every measurement interval, CodeCarbon calculates the `energy_delta` by subtracting the previously tracked `last_energy` from the current total energy reading.
+
+## 2. Power Estimation from Energy Deltas
+
+Instead of relying solely on instantaneous power sensors (which might not represent the whole interval due to microscopic spikes or drops between samples), CodeCarbon derives the average power over the latest measurement interval by backward-computing it from the total energy delta.
+
+The `Power.from_energies_and_delay` method handles this operation:
+
+```python
+delta_energy_kwh = float(abs(energy_now.kWh - energy_previous.kWh))
+power_kw = delta_energy_kwh / delay.hours
+```
+
+This conversion ensures that the computed power correctly reflects the true, steady average power usage across the whole measured time window (`delay`).
+
+## 3. Emitting Hardware Metrics
+
+The tracker has designated logic blocks for different components (e.g., CPU, RAM, GPU). Every `last_duration` seconds, each hardware component executes its `measure_power_and_energy()` method, taking the following steps for all monitored devices of that type:
+
+1. Retrieves device-level stats (via a `delta` operation), updating `last_energy` for the next cycle.
+2. Sums the total energy consumption safely into an aggregated Energy object.
+3. Sums all derived power usage (`power_kw` from the delta) across the devices into a Total Power object.
+
+## 4. Running Averages in the Main Emissions Tracker
+
+Inside the main `EmissionsTracker`, the energy values are securely accumulated over the session's lifespan.
+
+For recording the power, a running sum is maintained:
+
+- As CodeCarbon sequentially takes measurements, it tracks the output of `power.W`.
+- It dynamically increments running variables like `_gpu_power_sum`, `_cpu_power_sum`, `_ram_power_sum`.
+- It increments a global counter `_power_measurement_count`.
+
+At the end of an execution task (or when data is exported), the true average Power is formulated:
+```python
+avg_gpu_power = _gpu_power_sum / _power_measurement_count
+```
+This smoothing process prevents singular short measurement anomalies from skewing the final aggregated power values published in `EmissionsData`.
+
+## Summary Pipeline
+
+In short:
+
+1. **Hardware Counters (Accumulated Energy)**
+2. Subtract `last_energy` = **Energy Delta**
+3. Divide Energy Delta by `last_duration` = **Interval Average Power**
+4. Keep track of the sums of Interval Average Power
+5. Divide by number of samples = **Global Average Power representation**.
+
+## Challenges and Edge Cases
+
+Because power is derived from the difference between two accumulating numbers and a time delta, several edge cases can lead to anomalies (like sudden values of millions of Watts):
+
+### 1. Counter Wrapping and Resets
+Hardware counters have maximum bounds (e.g., 32-bit or 64-bit integers). Once they reach their maximum limit, they wrap around to zero. If the current energy is less than the previous energy, a naive calculation would be negative. CodeCarbon must detect this and safely handle the overflow to prevent negative power outputs. Similarly, if the hardware resets or the driver reloads mid-run, the counter might abruptly restart from 0.
+
+### 2. Micro-Intervals and Tiny Time Deltas
+If two measurements happen too close together (due to thread scheduling anomalies, initial configuration, or rapid manual tracking calls), the time delta (`last_duration`) becomes extremely small. Dividing even a tiny, expected energy delta by an artificially small time slice can cause the derived Power (W) to explode into mathematically huge numbers (e.g., measuring 2.5 million Watts), even if the underlying counter merely shifted by a fraction of a Joule.
+
+### 3. Multi-Chip Modules (MCM)
+Modern hardware, such as AMD's MI250X GPUs, often places multiple compute dies (GCDs) on a single package. The driver might expose energy counters that behave differently than expected (e.g., counters resetting to zero, or different sensors polling at different intervals). Misaligning the tracking scope or reading uninitialized accumulators early in the run can lead to wildly skewed deltas that propagate into massive power spikes.
+
+By relying heavily on energy accumulators rather than instantaneous power readings, CodeCarbon ensures a highly accurate sum of the total consumed energy. However, whenever you see an impossibly high "power" reading in the logs or emissions files, it is almost certainly a calculation artifact of dividing an unexpected energy delta by a time interval.
diff --git a/examples/pue.py b/examples/pue.py
index 28dd80497..1d5fac7c5 100644
--- a/examples/pue.py
+++ b/examples/pue.py
@@ -6,6 +6,7 @@
@track_emissions(
measure_power_secs=3,
pue=2,
+ log_level="DEBUG",
)
def train_model():
"""
diff --git a/examples/slurm_rocm/amdsmi_demo.py b/examples/slurm_rocm/amdsmi_demo.py
new file mode 100644
index 000000000..4f7618f8f
--- /dev/null
+++ b/examples/slurm_rocm/amdsmi_demo.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+
+import amdsmi
+
+
+def main():
+ try:
+ # Initialize AMD SMI
+ amdsmi.amdsmi_init()
+
+ # Get all GPU handles
+ devices = amdsmi.amdsmi_get_processor_handles()
+
+ if not devices:
+ print("No AMD GPUs detected.")
+ return
+
+ for idx, device in enumerate(devices):
+ print(f"\n===== GPU {idx} =====")
+
+ # Get GPU metrics
+ metrics = amdsmi.amdsmi_get_gpu_metrics_info(device)
+
+ # Energy (microjoules)
+ energy = metrics.get("energy_accumulator", None)
+
+ # Power (microwatts)
+ avg_power = metrics.get("average_socket_power", None)
+ cur_power = metrics.get("current_socket_power", None)
+
+ print(f"Energy accumulator : {energy} uJ")
+ print(f"Average socket power : {avg_power} W")
+ print(f"Current socket power : {cur_power} W")
+
+ amdsmi.amdsmi_shut_down()
+
+ except Exception as e:
+ print("Error:", str(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/slurm_rocm/amdsmi_demo.slurm b/examples/slurm_rocm/amdsmi_demo.slurm
new file mode 100644
index 000000000..52de4d673
--- /dev/null
+++ b/examples/slurm_rocm/amdsmi_demo.slurm
@@ -0,0 +1,55 @@
+#!/bin/bash
+#SBATCH --account=cad15147
+#SBATCH --constraint=MI250
+#SBATCH --nodes=1
+#SBATCH --time=0:25:00
+#SBATCH --gpus-per-node=1
+#SBATCH --ntasks-per-node=1
+#SBATCH --cpus-per-task=8
+#SBATCH --threads-per-core=1
+#SBATCH --job-name=codecarbon-test
+#SBATCH --output=logs/%j.out
+#SBATCH --error=logs/%j.err
+
+# Load AMD ROCM environment
+module purge
+module load cpe/24.07
+module load python/3.12
+module load rocm/7.0.1
+
+# Print environment info
+echo "=== Job Environment ==="
+echo "Running on host: $(hostname)"
+echo "at: $(date)"
+echo "Job ID: $SLURM_JOB_ID"
+echo "Number of GPUs: $SLURM_GPUS_PER_NODE"
+echo "ROCR_VISIBLE_DEVICES: $ROCR_VISIBLE_DEVICES"
+echo "HSA_OVERRIDE_GFX_VERSION: $HSA_OVERRIDE_GFX_VERSION"
+echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH"
+echo "PATH: $PATH"
+export PYTHONPATH=/opt/rocm-7.0.1/share/amd_smi:$PYTHONPATH
+echo "PYTHONPATH: $PYTHONPATH"
+
+rocm-smi
+rocm-smi --version
+rocm-smi --showmetrics --json
+
+
+# Create logs directory if it doesn't exist
+mkdir -p logs
+
+# Run the Python script
+pip install amdsmi==7.0.1
+echo "=== Installed AMD SMI Python Package ==="
+python3 -m pip list | grep -E "(amd)"
+echo "=== AMD SMI Metrics ==="
+amd-smi -h
+# Verify activation (optional)
+echo "=== Python Version ==="
+which python3
+python3 --version
+ls /opt/rocm-7.0.1/share/amd_smi
+echo "=== ls /opt ==="
+ls /opt
+echo "=== Running Training Script ==="
+srun python amdsmi_demo.py
diff --git a/examples/slurm_rocm/no_load.py b/examples/slurm_rocm/no_load.py
new file mode 100644
index 000000000..ef35328f3
--- /dev/null
+++ b/examples/slurm_rocm/no_load.py
@@ -0,0 +1,24 @@
+"""
+Use CodeCarbon but without loading the AMD GPU.
+pip install codecarbon
+"""
+
+import time
+
+from codecarbon import track_emissions
+
+
+@track_emissions(
+ measure_power_secs=5,
+ log_level="debug",
+)
+def train_model():
+ """
+ This function will do nothing.
+ """
+ print("10 seconds before ending script...")
+ time.sleep(10)
+
+
+if __name__ == "__main__":
+ model = train_model()
diff --git a/examples/slurm_rocm/pytorch_matrix.py b/examples/slurm_rocm/pytorch_matrix.py
new file mode 100644
index 000000000..39b889fae
--- /dev/null
+++ b/examples/slurm_rocm/pytorch_matrix.py
@@ -0,0 +1,321 @@
+"""
+Multi-GPU matrix multiplication example using PyTorch with ROCm AMD GPUs,
+designed to run for 2 minutes at 100% load.
+This script includes detailed logging of environment variables, GPU availability, memory usage, and computation progress.
+It also handles GPU memory allocation failures gracefully by attempting a smaller matrix size if the initial allocation fails.
+The script is decorated with CodeCarbon's `track_emissions` to measure energy consumption during the computation.
+
+Tested with:
+```
+# Look at https://download.pytorch.org/whl/torch/ for the correct version matching your Python (cp312) and ROCM version.
+# torch-2.10.0+rocm7.0-cp312-cp312-manylinux_2_28_x86_64.whl
+pip3 install torch==2.10.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.0
+pip install amdsmi==7.0.1
+```
+"""
+
+import logging
+import os
+import subprocess
+import sys
+import time
+from concurrent.futures import ThreadPoolExecutor
+
+import torch
+
+from codecarbon import track_emissions
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="[%(asctime)s] %(levelname)s - %(message)s",
+ handlers=[logging.StreamHandler(sys.stdout)],
+)
+logger = logging.getLogger(__name__)
+# Force flush after each log
+for handler in logger.handlers:
+ handler.flush = lambda: sys.stdout.flush()
+
+
+def _log_environment():
+ """Log environment variables and GPU availability."""
+ logger.info("Checking if ROCm/AMD GPU is available...")
+ logger.info(
+ f"ROCR_VISIBLE_DEVICES: {os.environ.get('ROCR_VISIBLE_DEVICES', 'not set')}"
+ )
+ logger.info(
+ f"HIP_VISIBLE_DEVICES: {os.environ.get('HIP_VISIBLE_DEVICES', 'not set')}"
+ )
+ logger.info(
+ f"CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES', 'not set')}"
+ )
+ sys.stdout.flush()
+
+
+def _select_device():
+ """Select and configure devices (all GPUs or CPU)."""
+ if not torch.cuda.is_available():
+ logger.warning("ROCm/AMD GPU is not available. Using CPU instead.")
+ return [torch.device("cpu")], 4096
+
+ num_gpus = torch.cuda.device_count()
+ logger.info(f"PyTorch sees {num_gpus} GPU(s)")
+
+ devices = [torch.device(f"cuda:{i}") for i in range(num_gpus)]
+ for i in range(len(devices)):
+ logger.info(f"GPU {i}: {torch.cuda.get_device_name(i)}")
+ sys.stdout.flush()
+
+ _log_gpu_memory_info(devices)
+ return devices, 4096
+
+
+def _log_gpu_memory_info(devices):
+ """Log GPU memory information if available."""
+ try:
+ for i, device in enumerate(devices):
+ if device.type == "cuda":
+ total_memory = torch.cuda.get_device_properties(i).total_memory / (
+ 1024**3
+ )
+ logger.info(f"GPU {i} - Total memory: {total_memory:.2f} GB")
+ logger.info(
+ f" Allocated: {torch.cuda.memory_allocated(i) / (1024**3):.2f} GB"
+ )
+ logger.info(
+ f" Cached: {torch.cuda.memory_reserved(i) / (1024**3):.2f} GB"
+ )
+ sys.stdout.flush()
+ except Exception as e:
+ logger.error(f"Could not get GPU memory info: {e}")
+ sys.stdout.flush()
+
+
+def _allocate_matrix(devices, matrix_size):
+ """Allocate matrix tensors on all devices with fallback to smaller size on failure."""
+ logger.info(
+ f"Allocating matrices of size {matrix_size}x{matrix_size} on {len(devices)} device(s)..."
+ )
+ logger.info(
+ f"Expected memory: ~{(matrix_size * matrix_size * 4) / (1024**3):.2f} GB per matrix per device"
+ )
+ logger.info("Creating matrices with fixed values...")
+ sys.stdout.flush()
+
+ matrices = []
+ try:
+ for i, device in enumerate(devices):
+ matrix = torch.full(
+ (matrix_size, matrix_size), 0.5, device=device, dtype=torch.float32
+ )
+ if device.type == "cuda":
+ torch.cuda.synchronize(device)
+ alloc_gb = torch.cuda.memory_allocated(i) / (1024**3)
+ logger.info(
+ f"Device {i}: Matrix created. GPU memory: {alloc_gb:.2f} GB"
+ )
+ matrices.append(matrix)
+ logger.info("All matrices created and initialized successfully")
+ sys.stdout.flush()
+ return matrices
+ except Exception as e:
+ logger.error(f"Failed to allocate matrices: {e}")
+ if any(d.type == "cuda" for d in devices):
+ logger.info("Trying with smaller matrix size (2048)...")
+ matrices = []
+ for device in devices:
+ matrix = torch.full(
+ (2048, 2048), 0.5, device=device, dtype=torch.float32
+ )
+ if device.type == "cuda":
+ torch.cuda.synchronize(device)
+ matrices.append(matrix)
+ logger.info("Matrices created successfully with reduced size 2048")
+ sys.stdout.flush()
+ return matrices
+ raise
+
+
+def _run_rocm_smi_check():
+ """Run rocm-smi command and log output."""
+ try:
+ result = subprocess.run(["rocm-smi"], capture_output=True, text=True, timeout=5)
+ logger.info("GPU visible to rocm-smi:")
+ logger.info(result.stdout)
+ except Exception as e:
+ logger.warning(f"Could not run rocm-smi: {e}")
+
+
+def _run_computation_on_device(device, matrix, duration):
+ """Run computation on a single device for the specified duration."""
+ start_time = time.time()
+ iteration = 0
+ result = None
+
+ while time.time() - start_time < duration:
+ result = torch.mm(matrix, matrix)
+ if device.type == "cuda":
+ torch.cuda.synchronize(device)
+ iteration += 1
+
+ return result, iteration, time.time() - start_time
+
+
+def _run_computation_loop(devices, matrices):
+ """Run the main computation loop for 120 seconds on all devices in parallel."""
+ logger.info(f"Starting computation loop on {len(devices)} device(s)...")
+ sys.stdout.flush()
+
+ start_time = time.time()
+ duration = 120
+ last_print_time = 0
+ results = []
+ iterations = []
+
+ with ThreadPoolExecutor(max_workers=len(devices)) as executor:
+ # Submit computation tasks for all devices
+ futures = []
+ for device, matrix in zip(devices, matrices):
+ future = executor.submit(
+ _run_computation_on_device, device, matrix, duration
+ )
+ futures.append(future)
+
+ # Monitor progress while computations run in parallel
+ while time.time() - start_time < duration:
+ elapsed = time.time() - start_time
+ if int(elapsed) // 10 > last_print_time // 10:
+ logger.info(
+ f"Progress: {elapsed:.1f}s / {duration}s (computing on {len(devices)} device(s))"
+ )
+ sys.stdout.flush()
+ last_print_time = elapsed
+ _run_rocm_smi_check()
+
+ # Collect results from all devices
+ for i, future in enumerate(futures):
+ result, iteration, elapsed = future.result()
+ results.append(result)
+ iterations.append(iteration)
+ logger.info(f"Device {i}: {iteration} iterations in {elapsed:.2f}s")
+
+ total_elapsed = time.time() - start_time
+ total_iterations = sum(iterations)
+ return results, total_iterations, total_elapsed
+
+
+def _cleanup_resources(devices, results, matrices):
+ """Clean up GPU and tensor resources."""
+ logger.info("Cleaning up resources...")
+ sys.stdout.flush()
+ del results
+ del matrices
+ for device in devices:
+ if device.type == "cuda":
+ torch.cuda.empty_cache()
+
+
+@track_emissions(
+ measure_power_secs=5,
+ log_level="debug",
+ offline=True,
+ country_iso_code="FRA",
+ pue=1.1,
+)
+def train_model():
+ """
+ Performs GPU-intensive computation for 2 minutes at 100% load using ROCm AMD GPU.
+ """
+ logger.info("=" * 60)
+ logger.info("STARTING TRAIN_MODEL FUNCTION")
+ logger.info("=" * 60)
+ sys.stdout.flush()
+
+ try:
+ _log_environment()
+ devices, matrix_size = _select_device()
+
+ logger.info(
+ f"Starting GPU-intensive computation for 120 seconds with matrix size {matrix_size}..."
+ )
+ sys.stdout.flush()
+
+ matrices = _allocate_matrix(devices, matrix_size)
+
+ if any(d.type == "cuda" for d in devices):
+ for i, device in enumerate(devices):
+ if device.type == "cuda":
+ logger.info(
+ f"Final GPU {i} memory: {torch.cuda.memory_allocated(i) / (1024**3):.2f} GB"
+ )
+ sys.stdout.flush()
+
+ results, total_iterations, elapsed = _run_computation_loop(devices, matrices)
+ _cleanup_resources(devices, results, matrices)
+
+ logger.info(
+ f"Completed! Total time: {elapsed:.2f}s, Total iterations: {total_iterations}"
+ )
+ logger.info("=" * 60)
+ sys.stdout.flush()
+
+ except RuntimeError as e:
+ logger.error(f"Runtime error occurred: {e}")
+ sys.stdout.flush()
+ if "out of memory" in str(e).lower():
+ logger.error("GPU out of memory. Try reducing matrix_size.")
+ sys.stdout.flush()
+ raise
+ except Exception as e:
+ logger.error(f"Unexpected error: {e}")
+ sys.stdout.flush()
+ raise
+
+
+if __name__ == "__main__":
+ logger.info("Starting training script...")
+ sys.stdout.flush()
+
+ # Pre-initialize PyTorch ROCm context BEFORE CodeCarbon starts its background thread
+ if torch.cuda.is_available():
+ logger.info("Pre-initializing PyTorch ROCm context...")
+ sys.stdout.flush()
+ try:
+ num_gpus = torch.cuda.device_count()
+ for gpu_id in range(num_gpus):
+ logger.info(f" Initializing GPU {gpu_id}...")
+ sys.stdout.flush()
+
+ logger.info(
+ f" Step 1: Setting up device targeting logical id {gpu_id}..."
+ )
+ sys.stdout.flush()
+ dev = torch.device(f"cuda:{gpu_id}")
+
+ logger.info(" Step 2: Checking memory parameters before alloc...")
+ sys.stdout.flush()
+ _ = torch.cuda.get_device_properties(gpu_id)
+
+ logger.info(" Step 3: Triggering C++ allocator backend...")
+ sys.stdout.flush()
+ # Try to force the memory caching allocator initialization directly using raw zero tensor which is more robust than scalar
+ a = torch.zeros((1,), device=dev)
+ logger.info(" Allocation complete.")
+ sys.stdout.flush()
+
+ logger.info(" Step 4: Synchronizing device...")
+ sys.stdout.flush()
+ torch.cuda.synchronize(dev)
+ logger.info(f" GPU {gpu_id} initialized successfully.")
+ sys.stdout.flush()
+
+ logger.info("All PyTorch ROCm contexts initialized successfully.")
+ sys.stdout.flush()
+ except Exception as e:
+ logger.error(f"PyTorch context initialization FAILED: {str(e)}")
+ sys.stdout.flush()
+ raise
+
+ model = train_model()
+ logger.info("Script finished.")
+ sys.stdout.flush()
diff --git a/examples/slurm_rocm/run_codecarbon_only.slurm b/examples/slurm_rocm/run_codecarbon_only.slurm
new file mode 100644
index 000000000..c746ac1fd
--- /dev/null
+++ b/examples/slurm_rocm/run_codecarbon_only.slurm
@@ -0,0 +1,46 @@
+#!/bin/bash
+#SBATCH --account=cad15147
+#SBATCH --constraint=MI250
+#SBATCH --nodes=1
+#SBATCH --time=0:25:00
+#SBATCH --gpus-per-node=1
+#SBATCH --ntasks-per-node=1
+#SBATCH --cpus-per-task=8
+#SBATCH --threads-per-core=1
+#SBATCH --job-name=codecarbon-test
+#SBATCH --output=logs/%j.out
+#SBATCH --error=logs/%j.err
+
+# Load AMD ROCM environment
+module purge
+module load cpe/24.07
+module load python/3.12
+module load rocm/7.0.1
+
+# Print environment info
+echo "=== Job Environment ==="
+echo "Running on host: $(hostname)"
+echo "at: $(date)"
+echo "Job ID: $SLURM_JOB_ID"
+echo "Number of GPUs: $SLURM_GPUS_PER_NODE"
+echo "ROCR_VISIBLE_DEVICES: $ROCR_VISIBLE_DEVICES"
+echo "HSA_OVERRIDE_GFX_VERSION: $HSA_OVERRIDE_GFX_VERSION"
+export PYTHONPATH=/opt/rocm-7.0.1/share/amd_smi:$PYTHONPATH
+
+rocm-smi
+rocm-smi --showpower --showtemp --showmeminfo vram --showenergy --json
+rocm-smi --version
+
+# Create logs directory if it doesn't exist
+mkdir -p logs
+
+# Run the Python script
+echo "=== Starting CodeCarbon Test ==="
+source .venv-codecarbon/bin/activate
+python3 -m pip list | grep -E "(torch|amd)"
+# Verify activation (optional)
+which python3
+python3 --version
+
+echo "=== Running Training Script ==="
+srun python no_load.py
diff --git a/examples/slurm_rocm/run_codecarbon_pytorch.slurm b/examples/slurm_rocm/run_codecarbon_pytorch.slurm
new file mode 100644
index 000000000..4e2c7b4aa
--- /dev/null
+++ b/examples/slurm_rocm/run_codecarbon_pytorch.slurm
@@ -0,0 +1,65 @@
+#!/bin/bash
+#SBATCH --account=cad15147
+#SBATCH --constraint=MI300
+#SBATCH --nodes=1
+#SBATCH --time=0:25:00
+#SBATCH --gpus-per-node=2
+#SBATCH --ntasks-per-node=1
+#SBATCH --cpus-per-task=8
+#SBATCH --threads-per-core=1
+#SBATCH --job-name=codecarbon-test
+#SBATCH --output=logs/%j.out
+#SBATCH --error=logs/%j.err
+
+# Load AMD ROCM environment
+module purge
+module load cpe/24.07
+module load rocm/7.0.1
+module load python/3.12
+export PYTHONPATH=/opt/rocm-7.0.1/share/amd_smi:$PYTHONPATH
+
+# Print environment info
+echo "=== Job Environment ==="
+echo "Running on host: $(hostname)"
+echo "at: $(date)"
+echo "Job ID: $SLURM_JOB_ID"
+echo "Number of GPUs: $SLURM_GPUS_PER_NODE"
+echo "ROCR_VISIBLE_DEVICES: $ROCR_VISIBLE_DEVICES"
+echo "HSA_OVERRIDE_GFX_VERSION: $HSA_OVERRIDE_GFX_VERSION"
+rocm-smi
+rocm-smi --version
+
+# Create logs directory if it doesn't exist
+mkdir -p logs
+
+# Run the Python script
+echo "=== Starting CodeCarbon Test ==="
+source .venv/bin/activate
+python3 -m pip list | grep -E "(torch|amd)"
+# Verify activation (optional)
+which python3
+python3 --version
+
+echo "=== PyTorch ROCm Test ==="
+python3 << 'EOF'
+import os
+import torch
+print(f"PyTorch version: {torch.__version__}")
+print(f"PyTorch built with CUDA: {torch.version.cuda}")
+print(f"PyTorch built with HIP: {torch.version.hip if hasattr(torch.version, 'hip') else 'N/A'}")
+print(f"CUDA available: {torch.cuda.is_available()}")
+print(f" ROCR_VISIBLE_DEVICES: {os.environ.get('ROCR_VISIBLE_DEVICES', 'not set')}")
+
+if torch.cuda.is_available():
+ print(f"Number of GPUs: {torch.cuda.device_count()}")
+ print(f"Current device: {torch.cuda.current_device()}")
+ print(f"Device name: {torch.cuda.get_device_name(0)}")
+else:
+ print("GPU NOT DETECTED - Checking environment:")
+ print(f" HIP_VISIBLE_DEVICES: {os.environ.get('HIP_VISIBLE_DEVICES', 'not set')}")
+ print(f" CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES', 'not set')}")
+ print(f" ROCR_VISIBLE_DEVICES: {os.environ.get('ROCR_VISIBLE_DEVICES', 'not set')}")
+EOF
+
+echo "=== Running Training Script ==="
+srun python examples/slurm_rocm/pytorch_matrix.py
diff --git a/mkdocs.yml b/mkdocs.yml
index 4517f6b98..f53c2aa52 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -113,6 +113,7 @@ nav:
- Motivation: introduction/motivation.md
- Methodology: introduction/methodology.md
- RAPL Metrics: introduction/rapl.md
+ - Power Estimation: introduction/power_estimation.md
- Model Comparisons: introduction/model_examples.md
- Frequently Asked Questions: introduction/faq.md
- Getting Started:
@@ -121,9 +122,12 @@ nav:
- CodeCarbon API: getting-started/api.md
- Parameters: getting-started/parameters.md
- Examples: getting-started/examples.md
- - Comet Integration: getting-started/comet.md
- - Advanced Installation: getting-started/advanced_installation.md
- Test on Scaleway: getting-started/test_on_scaleway.md
+ - Advanced Usage:
+ - Install CodeCarbon as a Linux service: advanced/linux_service.md
+ - Deploy with Ansible: advanced/ansible.md
+ - Comet Integration: getting-started/comet.md
+ - ROCm and PyTorch on SLURM SuperComputer: advanced/adastra.md
- Logging:
- Output: logging/output.md
- Collecting emissions to a logger: logging/to_logger.md
diff --git a/pyproject.toml b/pyproject.toml
index 028587aed..4b5cb17a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -111,7 +111,10 @@ viz-legacy = [
"dash_bootstrap_components > 1.0.0",
"fire",
]
-
+# To support AMD GPU
+amdsmi = [
+ "amdsmi>=6.0.0"
+]
[project.scripts]
carbonboard = "codecarbon.viz.carbonboard:main"
diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py
index f169d5d5c..0c3c49592 100644
--- a/tests/cli/test_cli_main.py
+++ b/tests/cli/test_cli_main.py
@@ -43,7 +43,7 @@ def test_monitor_offline_requires_country_iso_code():
runner = CliRunner()
result = runner.invoke(cli_main.codecarbon, ["monitor", "--offline"])
assert result.exit_code != 0
- assert "country_iso_code is required for offline mode" in result.output
+ assert "Country ISO code is required for offline mode" in result.output
def test_detect_monkeypatched_tracker(monkeypatch):
diff --git a/tests/test_config.py b/tests/test_config.py
index 6b6bac1b9..f263e7a42 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -7,6 +7,7 @@
from codecarbon.core.config import (
clean_env_key,
get_hierarchical_config,
+ normalize_gpu_ids,
parse_env_config,
parse_gpu_ids,
)
@@ -45,6 +46,20 @@ def test_parse_gpu_ids(self):
]:
self.assertEqual(parse_gpu_ids(ids), target)
+ def test_normalize_gpu_ids(self):
+ for ids, target in [
+ (None, None),
+ ("0,1,2", ["0", "1", "2"]),
+ ("MIG-f1e$%^", ["MIG-f1e"]),
+ ([1, 2, 3], [1, 2, 3]),
+ (
+ [0, "MIG-f1e$%^", "1, 2", "GPU-abcd!"],
+ [0, "MIG-f1e", "1", "2", "GPU-abcd"],
+ ),
+ ([0, {"invalid": "entry"}, "GPU-123"], [0, "GPU-123"]),
+ ]:
+ self.assertEqual(normalize_gpu_ids(ids), target)
+
@mock.patch.dict(
os.environ,
{
@@ -229,6 +244,34 @@ def test_too_much_gpu_ids_in_env(self):
# self.assertEqual(gpu_count, 0)
tracker.stop()
+ @mock.patch.dict(
+ os.environ,
+ {
+ "ROCR_VISIBLE_DEVICES": "1, 2",
+ },
+ )
+ def test_gpu_ids_from_rocr_visible_devices(self):
+ with patch("os.path.exists", return_value=True):
+ tracker = EmissionsTracker(
+ project_name="test-project", allow_multiple_runs=True
+ )
+ self.assertEqual(tracker._gpu_ids, ["1", "2"])
+
+ @mock.patch.dict(
+ os.environ,
+ {
+ "CUDA_VISIBLE_DEVICES": "0, 1",
+ "ROCR_VISIBLE_DEVICES": "1, 2",
+ },
+ )
+ def test_cuda_visible_devices_takes_precedence_over_rocr_visible_devices(self):
+ # CUDA_VISIBLE_DEVICES should take precedence as NVIDIA GPUs are checked first
+ with patch("os.path.exists", return_value=True):
+ tracker = EmissionsTracker(
+ project_name="test-project", allow_multiple_runs=True
+ )
+ self.assertEqual(tracker._gpu_ids, ["0", "1"])
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_core_util.py b/tests/test_core_util.py
index 6c1ba6f14..07c1bc47b 100644
--- a/tests/test_core_util.py
+++ b/tests/test_core_util.py
@@ -1,9 +1,16 @@
import shutil
import tempfile
+from unittest import mock
import pytest
-from codecarbon.core.util import backup, detect_cpu_model, is_mac_arm, resolve_path
+from codecarbon.core.util import (
+ backup,
+ count_cpus,
+ detect_cpu_model,
+ is_mac_arm,
+ resolve_path,
+)
def test_detect_cpu_model_caching():
@@ -72,3 +79,59 @@ def test_backup():
)
def test_is_mac_arm(cpu_model, expected):
assert is_mac_arm(cpu_model) == expected
+
+
+def test_count_cpus_no_slurm():
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", None):
+ with mock.patch("codecarbon.core.util.psutil.cpu_count", return_value=4):
+ assert count_cpus() == 4
+
+
+def test_count_cpus_slurm():
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", "12345"):
+ with mock.patch(
+ "codecarbon.core.util.subprocess.check_output"
+ ) as mock_subprocess_output:
+ mock_subprocess_output.return_value = b"NumCPUs=8 gres/gpu=2\n"
+ assert count_cpus() == 8
+
+
+def test_count_cpus_slurm_no_gpu():
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", "12345"):
+ with mock.patch(
+ "codecarbon.core.util.subprocess.check_output"
+ ) as mock_subprocess_output:
+ mock_subprocess_output.return_value = b"NumCPUs=16\n"
+ assert count_cpus() == 16
+
+
+def test_count_cpus_slurm_exception():
+ import subprocess
+
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", "12345"):
+ with mock.patch(
+ "codecarbon.core.util.subprocess.check_output",
+ side_effect=subprocess.CalledProcessError(1, "cmd"),
+ ):
+ with mock.patch("codecarbon.core.util.psutil.cpu_count", return_value=4):
+ assert count_cpus() == 4
+
+
+def test_count_cpus_slurm_malformed():
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", "12345"):
+ with mock.patch(
+ "codecarbon.core.util.subprocess.check_output",
+ return_value=b"Something Else\n",
+ ):
+ with mock.patch("codecarbon.core.util.psutil.cpu_count", return_value=4):
+ assert count_cpus() == 4
+
+
+def test_count_cpus_slurm_too_many_matches():
+ with mock.patch("codecarbon.core.util.SLURM_JOB_ID", "12345"):
+ with mock.patch(
+ "codecarbon.core.util.subprocess.check_output",
+ return_value=b"NumCPUs=8 NumCPUs=16\n",
+ ):
+ with mock.patch("codecarbon.core.util.psutil.cpu_count", return_value=4):
+ assert count_cpus() == 4
diff --git a/tests/test_cpu.py b/tests/test_cpu.py
index f38f33108..8e700adb9 100644
--- a/tests/test_cpu.py
+++ b/tests/test_cpu.py
@@ -6,7 +6,9 @@
import pytest
+from codecarbon.core.config import normalize_gpu_ids
from codecarbon.core.cpu import (
+ DEFAULT_POWER_PER_CORE,
TDP,
IntelPowerGadget,
IntelRAPL,
@@ -335,6 +337,23 @@ def test_get_matching_cpu(self):
tdp._get_matching_cpu(model, cpu_data, greedy=False),
)
+ def test_main_fallback_default_power_when_unknown_cpu(self):
+ with (
+ mock.patch(
+ "codecarbon.core.cpu.detect_cpu_model", return_value="Mystery CPU"
+ ),
+ mock.patch(
+ "codecarbon.core.cpu.TDP._get_cpu_power_from_registry",
+ return_value=None,
+ ),
+ mock.patch("codecarbon.core.cpu.is_psutil_available", return_value=True),
+ mock.patch("codecarbon.core.cpu.count_cpus", return_value=8),
+ ):
+ tdp = TDP()
+
+ self.assertEqual(tdp.model, "Mystery CPU")
+ self.assertEqual(tdp.tdp, 8 * DEFAULT_POWER_PER_CORE)
+
class TestResourceTrackerCPUTracking(unittest.TestCase):
def test_set_cpu_tracking_skips_tdp_when_rapl_available(self):
@@ -479,6 +498,115 @@ def __init__(self):
mocked_fallback.assert_called_once_with(fake_tdp, 80)
+class TestResourceTrackerGPUTracking(unittest.TestCase):
+ def test_normalize_gpu_ids_mixed_list_with_escaping(self):
+ class DummyTracker:
+ def __init__(self):
+ self._conf = {}
+ self._gpu_ids = [0, "MIG-f1e$%^", "1, 2", "GPU-abcd!"]
+ self._hardware = []
+
+ tracker = DummyTracker()
+ resource_tracker = ResourceTracker(tracker)
+
+ normalized_gpu_ids = normalize_gpu_ids(resource_tracker.tracker._gpu_ids)
+
+ self.assertEqual(normalized_gpu_ids, [0, "MIG-f1e", "1", "2", "GPU-abcd"])
+
+ def test_normalize_gpu_ids_mixed_list_ignores_invalid_entries(self):
+ class DummyTracker:
+ def __init__(self):
+ self._conf = {}
+ self._gpu_ids = [0, {"invalid": "entry"}, "GPU-123"]
+ self._hardware = []
+
+ tracker = DummyTracker()
+ resource_tracker = ResourceTracker(tracker)
+
+ normalized_gpu_ids = normalize_gpu_ids(resource_tracker.tracker._gpu_ids)
+
+ self.assertEqual(normalized_gpu_ids, [0, "GPU-123"])
+
+ def test_set_gpu_tracking_rocm_with_string_ids(self):
+ class DummyTracker:
+ def __init__(self):
+ self._conf = {}
+ self._gpu_ids = "0,1"
+ self._hardware = []
+
+ tracker = DummyTracker()
+ resource_tracker = ResourceTracker(tracker)
+ fake_devices = mock.Mock()
+ fake_devices.devices.get_gpu_static_info.return_value = [
+ {"name": "AMD Instinct MI300X"},
+ {"name": "AMD Instinct MI300X"},
+ ]
+
+ with (
+ mock.patch(
+ "codecarbon.core.resource_tracker.normalize_gpu_ids",
+ return_value=[0, 1],
+ ),
+ mock.patch(
+ "codecarbon.core.resource_tracker.gpu.is_nvidia_system",
+ return_value=False,
+ ),
+ mock.patch(
+ "codecarbon.core.resource_tracker.gpu.is_rocm_system",
+ return_value=True,
+ ),
+ mock.patch(
+ "codecarbon.core.resource_tracker.GPU.from_utils",
+ return_value=fake_devices,
+ ),
+ ):
+ resource_tracker.set_GPU_tracking()
+
+ self.assertEqual(tracker._gpu_ids, [0, 1])
+ self.assertEqual(tracker._conf["gpu_ids"], [0, 1])
+ self.assertEqual(tracker._conf["gpu_count"], 2)
+ self.assertEqual(resource_tracker.gpu_tracker, "amdsmi")
+ self.assertEqual(tracker._conf["gpu_model"], "2 x AMD Instinct MI300X")
+ self.assertEqual(tracker._hardware, [fake_devices])
+
+ def test_set_gpu_tracking_rocm_with_mixed_ids(self):
+ class DummyTracker:
+ def __init__(self):
+ self._conf = {}
+ self._gpu_ids = [0, "MIG-f1e$%^", "1, 2"]
+ self._hardware = []
+
+ tracker = DummyTracker()
+ resource_tracker = ResourceTracker(tracker)
+ fake_devices = mock.Mock()
+ fake_devices.devices.get_gpu_static_info.return_value = [
+ {"name": "AMD Instinct MI300X"},
+ {"name": "AMD Instinct MI300X"},
+ ]
+
+ with (
+ mock.patch(
+ "codecarbon.core.resource_tracker.gpu.is_nvidia_system",
+ return_value=False,
+ ),
+ mock.patch(
+ "codecarbon.core.resource_tracker.gpu.is_rocm_system",
+ return_value=True,
+ ),
+ mock.patch(
+ "codecarbon.core.resource_tracker.GPU.from_utils",
+ return_value=fake_devices,
+ ) as mocked_gpu_from_utils,
+ ):
+ resource_tracker.set_GPU_tracking()
+
+ expected_gpu_ids = [0, "MIG-f1e", "1", "2"]
+ mocked_gpu_from_utils.assert_called_once_with(expected_gpu_ids)
+ self.assertEqual(tracker._gpu_ids, expected_gpu_ids)
+ self.assertEqual(tracker._conf["gpu_ids"], expected_gpu_ids)
+ self.assertEqual(tracker._conf["gpu_count"], 2)
+
+
class TestPhysicalCPU(unittest.TestCase):
def test_count_physical_cpus_windows(self):
with mock.patch("platform.system", return_value="Windows"):
diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py
index f7d4b19f9..8309c57e8 100644
--- a/tests/test_emissions_tracker.py
+++ b/tests/test_emissions_tracker.py
@@ -49,6 +49,7 @@ def heavy_computation(run_time_secs: float = 3):
@mock.patch("codecarbon.core.gpu.pynvml", fake_pynvml)
+@mock.patch("codecarbon.core.gpu.is_nvidia_system", return_value=True)
@mock.patch("codecarbon.core.gpu.is_gpu_details_available", return_value=True)
@mock.patch(
"codecarbon.external.hardware.AllGPUDevices.get_gpu_details",
@@ -89,6 +90,7 @@ def test_carbon_tracker_TWO_GPU_PRIVATE_INFRA_CANADA(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
responses.add(
@@ -107,7 +109,7 @@ def test_carbon_tracker_TWO_GPU_PRIVATE_INFRA_CANADA(
self.assertGreaterEqual(
mocked_get_gpu_details.call_count, 2
) # at least 2 times in 5 seconds + once for init >= 3
- self.assertEqual(3, mocked_is_gpu_details_available.call_count)
+ self.assertEqual(2, mocked_is_gpu_details_available.call_count)
self.assertEqual(1, len(responses.calls))
self.assertEqual(
"https://get.geojs.io/v1/ip/geo.json", responses.calls[0].request.url
@@ -124,6 +126,7 @@ def test_carbon_tracker_timeout(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
@@ -151,6 +154,7 @@ def test_graceful_start_failure(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = EmissionsTracker(measure_power_secs=1, save_to_file=False)
@@ -169,6 +173,7 @@ def test_graceful_stop_failure(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = EmissionsTracker(measure_power_secs=1, save_to_file=False)
@@ -188,6 +193,7 @@ def test_decorator_ONLINE_NO_ARGS(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
responses.add(
@@ -215,6 +221,7 @@ def test_decorator_ONLINE_WITH_ARGS(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
responses.add(
@@ -241,6 +248,7 @@ def test_decorator_OFFLINE_NO_COUNTRY(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# WHEN
@@ -257,6 +265,7 @@ def test_decorator_OFFLINE_WITH_LOC_ARGS(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
@@ -280,6 +289,7 @@ def test_decorator_OFFLINE_WITH_CLOUD_ARGS(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
@@ -303,6 +313,7 @@ def test_offline_tracker_country_name(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = OfflineEmissionsTracker(
country_iso_code="USA",
@@ -325,6 +336,7 @@ def test_offline_tracker_invalid_headers(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = OfflineEmissionsTracker(
country_iso_code="USA",
@@ -357,6 +369,7 @@ def test_offline_tracker_valid_headers(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = OfflineEmissionsTracker(
country_iso_code="USA",
@@ -394,6 +407,7 @@ def test_carbon_tracker_online_context_manager_TWO_GPU_PRIVATE_INFRA_CANADA(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# GIVEN
responses.add(
@@ -411,7 +425,7 @@ def test_carbon_tracker_online_context_manager_TWO_GPU_PRIVATE_INFRA_CANADA(
self.assertGreaterEqual(
mocked_get_gpu_details.call_count, 2
) # at least 2 times in 5 seconds + once for init >= 3
- self.assertEqual(3, mocked_is_gpu_details_available.call_count)
+ self.assertEqual(2, mocked_is_gpu_details_available.call_count)
self.assertEqual(1, len(responses.calls))
self.assertEqual(
"https://get.geojs.io/v1/ip/geo.json", responses.calls[0].request.url
@@ -434,7 +448,8 @@ def test_task_energy_with_live_update_interference(
mock_log_values, # Class decorator
mocked_env_cloud_details, # Class decorator
mocked_get_gpu_details, # Class decorator
- mocked_is_gpu_details_available, # Class decorator (outermost relevant one)
+ mocked_is_gpu_details_available, # Class decorator
+ mocked_is_nvidia_system, # Class decorator (outermost relevant one)
):
# --- Test Setup ---
# Configure mocks to return specific, non-zero energy values
@@ -538,6 +553,7 @@ def test_carbon_tracker_offline_context_manager(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
with OfflineEmissionsTracker(
country_iso_code="USA", output_dir=self.temp_path
@@ -559,6 +575,7 @@ def test_scheduler_warning_suppressed_when_stopped(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
"""Test that scheduler warning is suppressed when scheduler is stopped."""
with EmissionsTracker(
@@ -601,6 +618,7 @@ def test_scheduler_warning_shown_when_running(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
"""Test that scheduler warning is shown when scheduler is running but delayed."""
with EmissionsTracker(
@@ -645,6 +663,7 @@ def test_get_detected_hardware(
mocked_get_gpu_details,
mocked_env_cloud_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
tracker = EmissionsTracker(save_to_file=False)
hardware_info = tracker.get_detected_hardware()
@@ -678,6 +697,7 @@ def test_cumulative_emissions_with_varying_intensity(
mocked_get_cloud_metadata_class,
mocked_get_gpu_details,
mocked_is_gpu_details_available,
+ mocked_is_nvidia_system,
):
# Setup mocks
mock_geo.return_value = mock.MagicMock(
diff --git a/tests/test_gpu.py b/tests/test_gpu.py
index 8433c1580..bfbc8e603 100644
--- a/tests/test_gpu.py
+++ b/tests/test_gpu.py
@@ -19,370 +19,204 @@
# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
-import os.path
+import builtins
+import importlib.util
import sys
-from copy import copy, deepcopy
-from unittest import TestCase, mock
+from types import ModuleType
+from unittest import mock
+
+
+class TestGpuImportWarnings:
+ def _exec_gpu_module(self, import_func, check_output):
+ base_spec = importlib.util.find_spec("codecarbon.core.gpu")
+ module_spec = importlib.util.spec_from_file_location(
+ "gpu_import_test", base_spec.origin
+ )
+ module = importlib.util.module_from_spec(module_spec)
+ for module_name in (
+ "codecarbon.core.gpu",
+ "codecarbon.core.gpu_amd",
+ "codecarbon.core.gpu_nvidia",
+ "codecarbon.core.gpu_device",
+ ):
+ sys.modules.pop(module_name, None)
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+ with (
+ mock.patch("subprocess.check_output", side_effect=check_output),
+ mock.patch.object(builtins, "__import__", new=import_func),
+ ):
+ module_spec.loader.exec_module(module)
+ return module
-import pynvml as real_pynvml
+ def test_import_warns_when_modules_missing(self):
+ real_import = builtins.__import__
-tc = TestCase()
+ def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
+ if name in ("pynvml", "amdsmi"):
+ raise ImportError("missing")
+ return real_import(name, globals, locals, fromlist, level)
+ def check_output(_cmd, *args, **kwargs):
+ return b"ok"
-class FakeGPUEnv:
- def setup_method(self):
- self.old_sys_path = copy(sys.path)
- fake_module_path = os.path.join(os.path.dirname(__file__), "fake_modules")
- sys.path.insert(0, fake_module_path)
-
- # Clean old modules
+ old_pynvml = sys.modules.pop("pynvml", None)
+ old_amdsmi = sys.modules.pop("amdsmi", None)
try:
- del sys.modules["pynvml"]
- except KeyError:
- pass
+ with mock.patch(
+ "codecarbon.external.logger.logger.warning"
+ ) as warning_mock:
+ self._exec_gpu_module(fake_import, check_output)
+ finally:
+ if old_pynvml is not None:
+ sys.modules["pynvml"] = old_pynvml
+ if old_amdsmi is not None:
+ sys.modules["amdsmi"] = old_amdsmi
+
+ messages = " ".join(str(c.args[0]) for c in warning_mock.call_args_list)
+ assert "pynvml is not available" in messages
+ assert "amdsmi is not available" in messages
+
+ def test_import_warns_when_pynvml_init_fails(self):
+ fake_pynvml = ModuleType("pynvml")
+
+ def nvml_init():
+ raise RuntimeError("boom")
+
+ fake_pynvml.nvmlInit = nvml_init
+ old_pynvml = sys.modules.get("pynvml")
+ sys.modules["pynvml"] = fake_pynvml
+
+ real_import = builtins.__import__
+
+ def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
+ if name == "amdsmi":
+ raise ImportError("missing")
+ return real_import(name, globals, locals, fromlist, level)
+
+ def check_output(cmd, *args, **kwargs):
+ if cmd[0] == "nvidia-smi":
+ return b"ok"
+ raise OSError("missing")
try:
- del sys.modules["codecarbon.core.gpu"]
- except KeyError:
- pass
-
- # Setup the state, strings are returned as bytes
- self.DETAILS = {
- "handle_0": {
- "name": b"GeForce GTX 1080",
- "uuid": b"uuid-1",
- "memory": real_pynvml.c_nvmlMemory_t(1024, 100, 924),
- "temperature": 75,
- "power_usage": 26,
- "total_energy_consumption": 1000,
- "power_limit": 149,
- "utilization_rate": real_pynvml.c_nvmlUtilization_t(96, 0),
- "compute_mode": 0,
- "compute_processes": [
- real_pynvml.c_nvmlProcessInfo_t(16, 1024 * 1024),
- real_pynvml.c_nvmlProcessInfo_t(32, 2 * 1024 * 1024),
- ],
- "graphics_processes": [],
- },
- "handle_1": {
- "name": b"GeForce GTX 1080",
- "uuid": b"uuid-2",
- "memory": real_pynvml.c_nvmlMemory_t(1024, 200, 824),
- "temperature": 79,
- "power_usage": 29,
- "total_energy_consumption": 800,
- "power_limit": 149,
- "utilization_rate": real_pynvml.c_nvmlUtilization_t(0, 100),
- "compute_mode": 2,
- "compute_processes": [],
- "graphics_processes": [
- real_pynvml.c_nvmlProcessInfo_t(8, 1024 * 1024 * 1024),
- real_pynvml.c_nvmlProcessInfo_t(64, 2 * 1024 * 1024 * 1024),
- ],
- },
- }
- self.expected = [
- {
- "name": "GeForce GTX 1080",
- "uuid": "uuid-1",
- "total_memory": 1024,
- "free_memory": 100,
- "used_memory": 924,
- "temperature": 75,
- "power_usage": 26,
- "power_limit": 149,
- "total_energy_consumption": 1000,
- "gpu_utilization": 96,
- "compute_mode": 0,
- "compute_processes": [
- {"pid": 16, "used_memory": 1024 * 1024},
- {"pid": 32, "used_memory": 2 * 1024 * 1024},
- ],
- "graphics_processes": [],
- },
- {
- "name": "GeForce GTX 1080",
- "uuid": "uuid-2",
- "total_memory": 1024,
- "free_memory": 200,
- "used_memory": 824,
- "temperature": 79,
- "power_usage": 29,
- "power_limit": 149,
- "total_energy_consumption": 800,
- "gpu_utilization": 0,
- "compute_mode": 2,
- "compute_processes": [],
- "graphics_processes": [
- {"pid": 8, "used_memory": 1024 * 1024 * 1024},
- {"pid": 64, "used_memory": 2 * 1024 * 1024 * 1024},
- ],
- },
- ]
- import pynvml
-
- pynvml.DETAILS = self.DETAILS
- pynvml.INIT_MOCK.reset_mock()
-
- def teardown_method(self):
- # Restore the old paths
- sys.path = self.old_sys_path
- try:
- del sys.modules["codecarbon.external.hardware"]
- except KeyError:
- pass
-
-
-class TestGpu(FakeGPUEnv):
- def test_is_gpu_details_available(self):
- from codecarbon.core.gpu import is_gpu_details_available
-
- assert is_gpu_details_available() is True
-
- def test_static_gpu_info(self):
- from codecarbon.core.gpu import AllGPUDevices
+ with mock.patch(
+ "codecarbon.external.logger.logger.warning"
+ ) as warning_mock:
+ self._exec_gpu_module(fake_import, check_output)
+ finally:
+ if old_pynvml is None:
+ sys.modules.pop("pynvml", None)
+ else:
+ sys.modules["pynvml"] = old_pynvml
+
+ assert any(
+ "pynvml initialization failed" in str(c.args[0])
+ for c in warning_mock.call_args_list
+ )
+
+ def test_import_warns_when_amdsmi_attribute_error(self):
+ fake_pynvml = ModuleType("pynvml")
+ fake_pynvml.nvmlInit = lambda: None
+ old_pynvml = sys.modules.get("pynvml")
+ sys.modules["pynvml"] = fake_pynvml
+
+ real_import = builtins.__import__
+
+ def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
+ if name == "amdsmi":
+ raise AttributeError("broken")
+ return real_import(name, globals, locals, fromlist, level)
+
+ def check_output(cmd, *args, **kwargs):
+ if cmd[0] == "rocm-smi":
+ return b"ok"
+ raise OSError("missing")
- alldevices = AllGPUDevices()
- expected = [
- {
- "name": "GeForce GTX 1080",
- "uuid": "uuid-1",
- "total_memory": 1024,
- "power_limit": 149,
- "gpu_index": 0,
- },
- {
- "name": "GeForce GTX 1080",
- "uuid": "uuid-2",
- "total_memory": 1024,
- "power_limit": 149,
- "gpu_index": 1,
- },
- ]
-
- assert alldevices.get_gpu_static_info() == expected
-
- def test_gpu_details(self):
- from codecarbon.core.gpu import AllGPUDevices
-
- alldevices = AllGPUDevices()
-
- assert alldevices.get_gpu_details() == self.expected
-
- def test_gpu_no_power_limit(self):
- import pynvml
-
- from codecarbon.core.gpu import AllGPUDevices
-
- def raiseException(handle):
- raise Exception("Some bad exception")
+ try:
+ with mock.patch(
+ "codecarbon.external.logger.logger.warning"
+ ) as warning_mock:
+ self._exec_gpu_module(fake_import, check_output)
+ finally:
+ if old_pynvml is None:
+ sys.modules.pop("pynvml", None)
+ else:
+ sys.modules["pynvml"] = old_pynvml
- pynvml.nvmlDeviceGetEnforcedPowerLimit = raiseException
- alldevices = AllGPUDevices()
+ assert any(
+ "amdsmi is not properly configured" in str(c.args[0])
+ for c in warning_mock.call_args_list
+ )
- expected_power_limit = deepcopy(self.expected)
- expected_power_limit[0]["power_limit"] = None
- expected_power_limit[1]["power_limit"] = None
- assert alldevices.get_gpu_details() == expected_power_limit
+class TestGpuMethods:
+ @mock.patch("codecarbon.core.gpu_amd.subprocess.check_output")
+ def test_is_rocm_system(self, mock_subprocess):
+ from codecarbon.core.gpu import is_rocm_system
- def test_gpu_not_ready(self):
- import pynvml
+ mock_subprocess.return_value = b"rocm-smi"
+ assert is_rocm_system()
- from codecarbon.core.gpu import AllGPUDevices
+ @mock.patch("codecarbon.core.gpu_amd.subprocess.check_output")
+ def test_is_rocm_system_fail(self, mock_subprocess):
+ import subprocess
- def raise_exception(handle):
- raise pynvml.NVMLError("System is not in ready state")
+ from codecarbon.core.gpu import is_rocm_system
- pynvml.nvmlDeviceGetTotalEnergyConsumption = raise_exception
- alldevices = AllGPUDevices()
+ mock_subprocess.side_effect = subprocess.CalledProcessError(1, "cmd")
+ assert not is_rocm_system()
- expected = deepcopy(self.expected)
- expected[0]["total_energy_consumption"] = None
- expected[1]["total_energy_consumption"] = None
+ @mock.patch("codecarbon.core.gpu_nvidia.subprocess.check_output")
+ def test_is_nvidia_system(self, mock_subprocess):
+ from codecarbon.core.gpu import is_nvidia_system
- assert alldevices.get_gpu_details() == expected
+ mock_subprocess.return_value = b"nvidia-smi"
+ assert is_nvidia_system()
- def test_gpu_metadata_total_power(self):
- """
- Get the total power of all GPUs
- """
- # Prepare
- from codecarbon.core.units import Energy, Power, Time
- from codecarbon.external.hardware import GPU
+ @mock.patch("codecarbon.core.gpu_nvidia.subprocess.check_output")
+ def test_is_nvidia_system_fail(self, mock_subprocess):
+ import subprocess
- energy_consumption = {
- "handle_0": [100_701, 180_001, 190_001],
- "handle_1": [149_702, 180_002, 200_002],
- }
+ from codecarbon.core.gpu import is_nvidia_system
- def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
- return energy_consumption[handle].pop(0)
+ mock_subprocess.side_effect = subprocess.CalledProcessError(1, "cmd")
+ assert not is_nvidia_system()
- gpu1_energy2 = Energy.from_millijoules(energy_consumption["handle_0"][1])
- gpu1_energy3 = Energy.from_millijoules(energy_consumption["handle_0"][2])
- gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][1])
- gpu2_energy3 = Energy.from_millijoules(energy_consumption["handle_1"][2])
- gpu2_power2 = Power.from_energies_and_delay(gpu1_energy2, gpu1_energy3, Time(5))
- gpu1_power2 = Power.from_energies_and_delay(gpu2_energy2, gpu2_energy3, Time(5))
- expected_power = gpu1_power2 + gpu2_power2
-
- with mock.patch(
- "pynvml.nvmlDeviceGetTotalEnergyConsumption",
- side_effect=mock_nvmlDeviceGetTotalEnergyConsumption,
- ):
- gpu = GPU.from_utils()
- gpu.measure_power_and_energy(5)
-
- assert expected_power.kW == gpu.total_power().kW
-
- def test_gpu_metadata_one_gpu_power(self):
- """
- Get the power of just one GPU even if there are more than 1
- """
- # Prepare
- from codecarbon.core.units import Energy, Power, Time
- from codecarbon.external.hardware import GPU
-
- energy_consumption_mock = {
- "handle_0": [100_701, 180_001, 190_001],
- "handle_1": [149_702, 180_002, 200_002],
- }
- energy_consumption = deepcopy(energy_consumption_mock)
-
- def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
- return energy_consumption_mock[handle].pop(0)
-
- with mock.patch(
- "pynvml.nvmlDeviceGetTotalEnergyConsumption",
- side_effect=mock_nvmlDeviceGetTotalEnergyConsumption,
- ):
- gpu = GPU.from_utils()
- gpu.measure_power_and_energy(5, gpu_ids=[1])
- print(energy_consumption)
- gpu2_energy1 = Energy.from_millijoules(energy_consumption["handle_1"][1])
- gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][2])
- gpu2_power = Power.from_energies_and_delay(gpu2_energy1, gpu2_energy2, Time(5))
- expected_power = gpu2_power
-
- assert expected_power.kW == gpu.total_power().kW
-
- @mock.patch.dict(
- os.environ,
- {
- "CUDA_VISIBLE_DEVICES": "1",
- },
- )
- def test_gpu_metadata_one_gpu_power_CUDA_VISIBLE_DEVICES(self):
- """
- Get the power of just one GPU even if there are more than 1
- """
- # Prepare
- # (Note: This imports should be inside the test, not on top of the file, otherwise the mock does not work)
- from codecarbon.core.units import Energy, Power, Time
- from codecarbon.external.hardware import GPU
-
- energy_consumption_mock = {
- "handle_0": [100_000, 100_001, 100_002],
- "handle_1": [149_702, 180_002, 200_002],
- }
- energy_consumption = deepcopy(energy_consumption_mock)
-
- def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
- # print("mock_nvmlDeviceGetTotalEnergyConsumption", handle, energy_consumption_mock[handle])
- return energy_consumption_mock[handle].pop(0)
-
- # Call
- with mock.patch(
- "pynvml.nvmlDeviceGetTotalEnergyConsumption",
- side_effect=mock_nvmlDeviceGetTotalEnergyConsumption, # Mock the energy consumption
- ):
- gpu = GPU.from_utils(gpu_ids=[int(os.environ["CUDA_VISIBLE_DEVICES"])])
- # Despite the fact that there are 2 GPUs, only one is being used
- assert gpu.gpu_ids == [1]
- gpu.measure_power_and_energy(5)
-
- # Assert
- # ((200_002 - 180_002) * 10 ** (-3)) * 2.77778e-7 * 3_600 /5 = 0.0040000031999999994 kW
- gpu2_energy1 = Energy.from_millijoules(energy_consumption["handle_1"][1])
- gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][2])
- gpu2_power = Power.from_energies_and_delay(gpu2_energy1, gpu2_energy2, Time(5))
- expected_power = gpu2_power
- tc.assertAlmostEqual(expected_power.kW, gpu.total_power().kW)
-
- def test_get_gpu_ids(self):
- """
- Check parsing of gpu_ids in various forms.
- """
- # Prepare
- from codecarbon.external.hardware import GPU
-
- for test_ids, expected_ids in [
- ([0, 1], [0, 1]),
- ([0, 1, 2], [0, 1]),
- ([2], []),
- (["0", "1"], [0, 1]),
- # Only two GPUS in the system, so ignore the third (index 2)
- (["0", "1", "2"], [0, 1]),
- (["2"], []),
- # Check UUID-to-index mapping
- (["uuid-1"], [0]),
- (["uuid-1", "uuid-2"], [0, 1]),
- (["uuid-3"], []),
- # Check UUID-to-index mapping when we need to strip the prefix
- (["MIG-uuid-1"], [0]),
- (["MIG-uuid-3"], []),
- ]:
- gpu = GPU(test_ids)
- result = gpu._get_gpu_ids()
- assert result == expected_ids
-
-
-class TestGpuNotAvailable:
+class TestGpuTracking:
def setup_method(self):
- self.old_sys_path = copy(sys.path)
- fake_module_path = os.path.join(os.path.dirname(__file__), "fake_modules")
- sys.path.insert(0, fake_module_path)
-
- # Clean old modules
- try:
- del sys.modules["pynvml"]
- except KeyError:
- pass
-
- try:
- del sys.modules["codecarbon.core.gpu"]
- except KeyError:
- pass
-
- import pynvml
-
- pynvml.INIT_MOCK.side_effect = pynvml.NVMLError("NVML Shared Library Not Found")
-
- def teardown_method(self):
- import pynvml
-
- pynvml.INIT_MOCK.reset_mock()
-
- # Restore the old paths
- sys.path = self.old_sys_path
-
- def test_is_gpu_details_not_available(self):
- from codecarbon.core.gpu import is_gpu_details_available
-
- assert is_gpu_details_available() is False
-
- def test_gpu_details_not_available(self):
+ for module_name in list(sys.modules.keys()):
+ if module_name.startswith("codecarbon.core.gpu"):
+ sys.modules.pop(module_name, None)
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+
+ @mock.patch("codecarbon.core.gpu.is_rocm_system", return_value=True)
+ @mock.patch("codecarbon.core.gpu.is_nvidia_system", return_value=False)
+ @mock.patch("codecarbon.core.gpu_amd.subprocess.check_output")
+ def test_rocm_initialization(self, mock_subprocess, mock_nvidia, mock_rocm):
from codecarbon.core.gpu import AllGPUDevices
- alldevices = AllGPUDevices()
+ # Should not crash on init
+ AllGPUDevices()
- assert alldevices.get_gpu_details() == []
-
- def test_static_gpu_info_not_available(self):
+ @mock.patch("codecarbon.core.gpu.is_rocm_system", return_value=False)
+ @mock.patch("codecarbon.core.gpu.is_nvidia_system", return_value=True)
+ @mock.patch("codecarbon.core.gpu_nvidia.subprocess.check_output")
+ def test_nvidia_initialization(self, mock_subprocess, mock_nvidia, mock_rocm):
from codecarbon.core.gpu import AllGPUDevices
- alldevices = AllGPUDevices()
-
- assert alldevices.get_gpu_static_info() == []
+ # Should not crash on init
+ AllGPUDevices()
diff --git a/tests/test_gpu_amd.py b/tests/test_gpu_amd.py
new file mode 100644
index 000000000..72b39e54b
--- /dev/null
+++ b/tests/test_gpu_amd.py
@@ -0,0 +1,499 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2020 [COMET-ML]
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this
+# software and associated documentation files (the "Software"), to deal in the Software
+# without restriction, including without limitation the rights to use, copy, modify,
+# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+from types import SimpleNamespace
+from unittest import mock
+
+import pytest
+
+
+class TestAmdGpu:
+ def test_reinit_on_amdsmi_not_initialized_error(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ class FakeAmdSmiLibraryException(Exception):
+ def __init__(self, ret_code):
+ self.ret_code = ret_code
+ super().__init__(
+ f"Error code:\n {ret_code} | AMDSMI_STATUS_NOT_INIT - Device not initialized"
+ )
+
+ call_counter = {"count": 0}
+
+ def flaky_vram_usage(_handle):
+ if call_counter["count"] == 0:
+ call_counter["count"] += 1
+ raise FakeAmdSmiLibraryException(32)
+ return {"vram_total": 1000, "vram_used": 250}
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_exception=SimpleNamespace(
+ AmdSmiLibraryException=FakeAmdSmiLibraryException
+ ),
+ amdsmi_init=mock.MagicMock(),
+ amdsmi_get_gpu_vram_usage=mock.MagicMock(side_effect=flaky_vram_usage),
+ )
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ memory = device._get_memory_info()
+
+ assert fake_amdsmi.amdsmi_init.call_count == 1
+ assert fake_amdsmi.amdsmi_get_gpu_vram_usage.call_count == 2
+ assert memory.total == 1000 * 1024 * 1024
+ assert memory.used == 250 * 1024 * 1024
+ assert memory.free == 750 * 1024 * 1024
+
+ def test_no_reinit_on_other_amdsmi_library_error(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ class FakeAmdSmiLibraryException(Exception):
+ def __init__(self, ret_code):
+ self.ret_code = ret_code
+ super().__init__(
+ f"Error code:\n {ret_code} | SOME_OTHER_AMDSMI_ERROR"
+ )
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_exception=SimpleNamespace(
+ AmdSmiLibraryException=FakeAmdSmiLibraryException
+ ),
+ amdsmi_init=mock.MagicMock(),
+ amdsmi_get_gpu_vram_usage=mock.MagicMock(
+ side_effect=FakeAmdSmiLibraryException(31)
+ ),
+ )
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ with pytest.raises(FakeAmdSmiLibraryException):
+ device._get_memory_info()
+
+ assert fake_amdsmi.amdsmi_init.call_count == 0
+ assert fake_amdsmi.amdsmi_get_gpu_vram_usage.call_count == 1
+
+ def test_warn_dual_gcd_models_generic_once_device_specific_each_selection(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ AMDGPUDevice._dual_gcd_warning_emitted = False
+
+ device_1 = AMDGPUDevice.__new__(AMDGPUDevice)
+ device_1.gpu_index = 0
+ device_1._get_gpu_name = mock.MagicMock(return_value="AMD Instinct MI300X")
+ device_1._get_uuid = mock.MagicMock(return_value="uuid-1")
+ device_1._get_power_limit = mock.MagicMock(return_value=700)
+ device_1._get_memory_info = mock.MagicMock(
+ return_value=SimpleNamespace(total=1024)
+ )
+
+ device_2 = AMDGPUDevice.__new__(AMDGPUDevice)
+ device_2.gpu_index = 1
+ device_2._get_gpu_name = mock.MagicMock(return_value="AMD Instinct MI300X")
+ device_2._get_uuid = mock.MagicMock(return_value="uuid-2")
+ device_2._get_power_limit = mock.MagicMock(return_value=700)
+ device_2._get_memory_info = mock.MagicMock(
+ return_value=SimpleNamespace(total=1024)
+ )
+
+ with mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock:
+ device_1._init_static_details()
+ device_2._init_static_details()
+ device_1.emit_selection_warning()
+ device_2.emit_selection_warning()
+
+ assert device_1._known_zero_energy_counter is True
+ assert device_2._known_zero_energy_counter is True
+ # Generic warning is emitted once, then one device-specific warning per selected device
+ assert warning_mock.call_count == 3
+
+ AMDGPUDevice._dual_gcd_warning_emitted = False
+
+ def test_get_total_energy_consumption_returns_zero_for_known_dual_gcd_model(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_energy_count=mock.MagicMock())
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._known_zero_energy_counter = True
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"energy_accumulator": 0, "counter_resolution": 1000}
+ )
+ device._get_gpu_metrics_info = mock.MagicMock(
+ return_value={"energy_accumulator": 0}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ result = device._get_total_energy_consumption()
+
+ assert result == 0
+
+ def test_get_total_energy_consumption_returns_none_for_other_models(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_energy_count=mock.MagicMock())
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._known_zero_energy_counter = False
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"energy_accumulator": 0, "counter_resolution": 1000}
+ )
+ device._get_gpu_metrics_info = mock.MagicMock(
+ return_value={"energy_accumulator": 0}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ result = device._get_total_energy_consumption()
+
+ assert result is None
+
+ def test_is_dual_gcd_power_limited_model_mi210(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ assert device._is_dual_gcd_power_limited_model("AMD Instinct MI210") is False
+
+ def test_emit_selection_warning_noop_when_not_dual_gcd(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device._known_zero_energy_counter = False
+ device.gpu_index = 0
+ device._gpu_name = "AMD Instinct MI100"
+
+ with mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock:
+ device.emit_selection_warning()
+
+ warning_mock.assert_not_called()
+
+ def test_get_gpu_metrics_info_calls_amdsmi(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_gpu_metrics_info=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(return_value={"ok": True})
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ result = device._get_gpu_metrics_info()
+
+ device._call_amdsmi_with_reinit.assert_called_once_with(
+ fake_amdsmi.amdsmi_get_gpu_metrics_info, "fake_handle"
+ )
+ assert result == {"ok": True}
+
+ def test_get_total_energy_consumption_uses_power_key(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_energy_count=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"power": 123, "counter_resolution": 1000}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ result = device._get_total_energy_consumption()
+
+ assert result == 123
+
+ def test_get_total_energy_consumption_missing_keys_warns(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_energy_count=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"counter_resolution": 1000}
+ )
+
+ with (
+ mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True),
+ mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock,
+ ):
+ result = device._get_total_energy_consumption()
+
+ assert result is None
+ warning_mock.assert_called()
+
+ def test_get_total_energy_consumption_exception_warns(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_energy_count=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(side_effect=Exception("boom"))
+
+ with (
+ mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True),
+ mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock,
+ ):
+ result = device._get_total_energy_consumption()
+
+ assert result is None
+ warning_mock.assert_called()
+
+ def test_get_gpu_name_success_and_failure(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_gpu_asic_info=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"market_name": "AMD Instinct MI100"}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_gpu_name() == "AMD Instinct MI100"
+
+ device._call_amdsmi_with_reinit = mock.MagicMock(side_effect=Exception("boom"))
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_gpu_name() == "Unknown GPU"
+
+ def test_get_uuid(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_gpu_device_uuid=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(return_value="uuid-123")
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_uuid() == "uuid-123"
+
+ def test_get_temperature_fallback_and_exception(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ class FakeAmdSmiLibraryException(Exception):
+ pass
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_exception=SimpleNamespace(
+ AmdSmiLibraryException=FakeAmdSmiLibraryException
+ ),
+ AmdSmiTemperatureType=SimpleNamespace(HOTSPOT="hotspot"),
+ AmdSmiTemperatureMetric=SimpleNamespace(CURRENT="current"),
+ amdsmi_get_temp_metric=mock.MagicMock(),
+ )
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(return_value=0)
+ device._get_gpu_metrics_info = mock.MagicMock(
+ return_value={"temperature_hotspot": 42}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_temperature() == 42
+
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ side_effect=FakeAmdSmiLibraryException("fail")
+ )
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_temperature() == 0
+
+ def test_get_power_usage_fallback_paths(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_power_info=mock.MagicMock())
+
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"average_socket_power": "bad"}
+ )
+ device._get_gpu_metrics_info = mock.MagicMock(
+ return_value={"average_socket_power": 75}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_power_usage() == 75
+
+ device._get_gpu_metrics_info = mock.MagicMock(
+ return_value={"average_socket_power": "bad"}
+ )
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_power_usage() == 0
+
+ def test_get_power_limit_success_and_exception(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_power_cap_info=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"power_cap": 2_000_000}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_power_limit() == 2
+
+ device._call_amdsmi_with_reinit = mock.MagicMock(side_effect=Exception("boom"))
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_power_limit() is None
+
+ def test_get_gpu_utilization_and_compute_mode(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_gpu_activity=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value={"gfx_activity": 87}
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_gpu_utilization() == 87
+ assert device._get_compute_mode() is None
+
+ def test_get_compute_and_graphics_processes(self):
+ from codecarbon.core.gpu import AMDGPUDevice
+
+ fake_amdsmi = SimpleNamespace(amdsmi_get_gpu_process_list=mock.MagicMock())
+ device = AMDGPUDevice.__new__(AMDGPUDevice)
+ device.handle = "fake_handle"
+ device._call_amdsmi_with_reinit = mock.MagicMock(
+ return_value=[
+ {"pid": 1, "mem": 10, "engine_usage": {"gfx": 0}},
+ {"pid": 2, "mem": 20, "engine_usage": {"gfx": 5}},
+ ]
+ )
+
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_compute_processes() == [
+ {"pid": 1, "used_memory": 10},
+ {"pid": 2, "used_memory": 20},
+ ]
+ assert device._get_graphics_processes() == [{"pid": 2, "used_memory": 20}]
+
+ device._call_amdsmi_with_reinit = mock.MagicMock(side_effect=Exception("boom"))
+ with mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True):
+ assert device._get_compute_processes() == []
+ assert device._get_graphics_processes() == []
+
+
+class TestAllGPUDevicesAmd:
+ def test_init_with_no_amd_handles(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_init=mock.MagicMock(),
+ amdsmi_get_processor_handles=mock.MagicMock(return_value=[]),
+ amdsmi_get_gpu_device_uuid=mock.MagicMock(return_value="uuid"),
+ )
+
+ with (
+ mock.patch("codecarbon.core.gpu.AMDSMI_AVAILABLE", True),
+ mock.patch("codecarbon.core.gpu.PYNVML_AVAILABLE", False),
+ mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True),
+ mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock,
+ ):
+ AllGPUDevices()
+
+ warning_mock.assert_called_once_with(
+ "No AMD GPUs found on machine with amdsmi_get_processor_handles() !"
+ )
+
+ def test_init_with_amd_handles_and_bdf_fallback(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ class DummyAmdDevice:
+ def __init__(self, handle, gpu_index):
+ self.handle = handle
+ self.gpu_index = gpu_index
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_init=mock.MagicMock(),
+ amdsmi_get_processor_handles=mock.MagicMock(return_value=["h1", "h2"]),
+ amdsmi_get_gpu_device_bdf=mock.MagicMock(
+ side_effect=["0000:01:00.0", Exception("boom")]
+ ),
+ amdsmi_get_gpu_device_uuid=mock.MagicMock(
+ side_effect=lambda handle: f"uuid-{handle}"
+ ),
+ )
+
+ with (
+ mock.patch("codecarbon.core.gpu.AMDSMI_AVAILABLE", True),
+ mock.patch("codecarbon.core.gpu.PYNVML_AVAILABLE", False),
+ mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True),
+ mock.patch("codecarbon.core.gpu.AMDGPUDevice", DummyAmdDevice),
+ ):
+ devices = AllGPUDevices()
+
+ assert [d.handle for d in devices.devices] == ["h1", "h2"]
+
+ def test_init_amd_exception_warns(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ class FakeAmdSmiException(Exception):
+ pass
+
+ fake_amdsmi = SimpleNamespace(
+ amdsmi_init=mock.MagicMock(side_effect=FakeAmdSmiException("boom")),
+ AmdSmiException=FakeAmdSmiException,
+ )
+
+ with (
+ mock.patch("codecarbon.core.gpu.AMDSMI_AVAILABLE", True),
+ mock.patch("codecarbon.core.gpu.PYNVML_AVAILABLE", False),
+ mock.patch("codecarbon.core.gpu_amd.amdsmi", fake_amdsmi, create=True),
+ mock.patch("codecarbon.core.gpu.logger.warning") as warning_mock,
+ ):
+ AllGPUDevices()
+
+ warning_mock.assert_called()
+
+ def test_methods_handle_exceptions_and_start(self):
+ from codecarbon.core.gpu import AllGPUDevices
+ from codecarbon.core.units import Time
+
+ class ExplodingDevice:
+ def __init__(self):
+ self.started = False
+
+ def start(self):
+ self.started = True
+
+ def get_static_details(self):
+ raise RuntimeError("boom")
+
+ def get_gpu_details(self):
+ raise RuntimeError("boom")
+
+ def delta(self, _duration):
+ raise RuntimeError("boom")
+
+ devices = AllGPUDevices.__new__(AllGPUDevices)
+ exploding = ExplodingDevice()
+ devices.devices = [exploding]
+ devices.device_count = 1
+
+ devices.start()
+ assert exploding.started is True
+ assert devices.get_gpu_static_info() == []
+ assert devices.get_gpu_details() == []
+ assert devices.get_delta(Time(1)) == []
diff --git a/tests/test_gpu_nvidia.py b/tests/test_gpu_nvidia.py
new file mode 100644
index 000000000..99a1c86a8
--- /dev/null
+++ b/tests/test_gpu_nvidia.py
@@ -0,0 +1,434 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2020 [COMET-ML]
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this
+# software and associated documentation files (the "Software"), to deal in the Software
+# without restriction, including without limitation the rights to use, copy, modify,
+# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+import os.path
+import sys
+from copy import copy, deepcopy
+from unittest import TestCase, mock
+
+import pynvml as real_pynvml
+
+tc = TestCase()
+
+
+class FakeGPUEnv:
+ def setup_method(self):
+ self.old_sys_path = copy(sys.path)
+ fake_module_path = os.path.join(os.path.dirname(__file__), "fake_modules")
+ sys.path.insert(0, fake_module_path)
+
+ # Clean old modules
+ try:
+ del sys.modules["pynvml"]
+ except KeyError:
+ pass
+
+ try:
+ del sys.modules["codecarbon.core.gpu"]
+ except KeyError:
+ pass
+ for module_name in (
+ "codecarbon.core.gpu_amd",
+ "codecarbon.core.gpu_nvidia",
+ "codecarbon.core.gpu_device",
+ ):
+ sys.modules.pop(module_name, None)
+
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+
+ # Setup the state, strings are returned as bytes
+ self.DETAILS = {
+ "handle_0": {
+ "name": b"GeForce GTX 1080",
+ "uuid": b"uuid-1",
+ "memory": real_pynvml.c_nvmlMemory_t(1024, 100, 924),
+ "temperature": 75,
+ "power_usage": 26000,
+ "total_energy_consumption": 1000,
+ "power_limit": 149000,
+ "utilization_rate": real_pynvml.c_nvmlUtilization_t(96, 0),
+ "compute_mode": 0,
+ "compute_processes": [
+ real_pynvml.c_nvmlProcessInfo_t(16, 1024 * 1024),
+ real_pynvml.c_nvmlProcessInfo_t(32, 2 * 1024 * 1024),
+ ],
+ "graphics_processes": [],
+ },
+ "handle_1": {
+ "name": b"GeForce GTX 1080",
+ "uuid": b"uuid-2",
+ "memory": real_pynvml.c_nvmlMemory_t(1024, 200, 824),
+ "temperature": 79,
+ "power_usage": 29000,
+ "total_energy_consumption": 800,
+ "power_limit": 149000,
+ "utilization_rate": real_pynvml.c_nvmlUtilization_t(0, 100),
+ "compute_mode": 2,
+ "compute_processes": [],
+ "graphics_processes": [
+ real_pynvml.c_nvmlProcessInfo_t(8, 1024 * 1024 * 1024),
+ real_pynvml.c_nvmlProcessInfo_t(64, 2 * 1024 * 1024 * 1024),
+ ],
+ },
+ }
+ self.expected = [
+ {
+ "name": "GeForce GTX 1080",
+ "uuid": "uuid-1",
+ "gpu_index": 0,
+ "total_memory": 1024,
+ "free_memory": 100,
+ "used_memory": 924,
+ "temperature": 75,
+ "power_usage": 26,
+ "power_limit": 149,
+ "total_energy_consumption": 1000,
+ "gpu_utilization": 96,
+ "compute_mode": 0,
+ "compute_processes": [
+ {"pid": 16, "used_memory": 1024 * 1024},
+ {"pid": 32, "used_memory": 2 * 1024 * 1024},
+ ],
+ "graphics_processes": [],
+ },
+ {
+ "name": "GeForce GTX 1080",
+ "uuid": "uuid-2",
+ "gpu_index": 1,
+ "total_memory": 1024,
+ "free_memory": 200,
+ "used_memory": 824,
+ "temperature": 79,
+ "power_usage": 29,
+ "power_limit": 149,
+ "total_energy_consumption": 800,
+ "gpu_utilization": 0,
+ "compute_mode": 2,
+ "compute_processes": [],
+ "graphics_processes": [
+ {"pid": 8, "used_memory": 1024 * 1024 * 1024},
+ {"pid": 64, "used_memory": 2 * 1024 * 1024 * 1024},
+ ],
+ },
+ ]
+ import pynvml
+
+ pynvml.DETAILS = self.DETAILS
+ pynvml.INIT_MOCK.reset_mock()
+
+ def teardown_method(self):
+ # Restore the old paths
+ sys.path = self.old_sys_path
+ try:
+ del sys.modules["codecarbon.external.hardware"]
+ except KeyError:
+ pass
+
+
+class TestGpu(FakeGPUEnv):
+ def test_is_gpu_details_available(self):
+ from codecarbon.core.gpu import is_gpu_details_available
+
+ assert is_gpu_details_available() is True
+
+ def test_static_gpu_info(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ alldevices = AllGPUDevices()
+ expected = [
+ {
+ "name": "GeForce GTX 1080",
+ "uuid": "uuid-1",
+ "total_memory": 1024,
+ "power_limit": 149,
+ "gpu_index": 0,
+ },
+ {
+ "name": "GeForce GTX 1080",
+ "uuid": "uuid-2",
+ "total_memory": 1024,
+ "power_limit": 149,
+ "gpu_index": 1,
+ },
+ ]
+
+ assert alldevices.get_gpu_static_info() == expected
+
+ def test_gpu_details(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ alldevices = AllGPUDevices()
+
+ assert alldevices.get_gpu_details() == self.expected
+
+ def test_gpu_no_power_limit(self):
+ import pynvml
+
+ from codecarbon.core.gpu import AllGPUDevices
+
+ def raiseException(handle):
+ raise Exception("Some bad exception")
+
+ original_limit = pynvml.nvmlDeviceGetEnforcedPowerLimit
+ try:
+ pynvml.nvmlDeviceGetEnforcedPowerLimit = raiseException
+ alldevices = AllGPUDevices()
+
+ expected_power_limit = deepcopy(self.expected)
+ expected_power_limit[0]["power_limit"] = None
+ expected_power_limit[1]["power_limit"] = None
+
+ assert alldevices.get_gpu_details() == expected_power_limit
+ finally:
+ pynvml.nvmlDeviceGetEnforcedPowerLimit = original_limit
+
+ def test_gpu_not_ready(self):
+ import pynvml
+
+ from codecarbon.core.gpu import AllGPUDevices
+
+ def raise_exception(handle):
+ raise pynvml.NVMLError("System is not in ready state")
+
+ original_energy = pynvml.nvmlDeviceGetTotalEnergyConsumption
+ try:
+ pynvml.nvmlDeviceGetTotalEnergyConsumption = raise_exception
+ alldevices = AllGPUDevices()
+
+ expected = deepcopy(self.expected)
+ expected[0]["total_energy_consumption"] = None
+ expected[1]["total_energy_consumption"] = None
+
+ assert alldevices.get_gpu_details() == expected
+ finally:
+ pynvml.nvmlDeviceGetTotalEnergyConsumption = original_energy
+
+ def test_gpu_metadata_total_power(self):
+ """
+ Get the total power of all GPUs
+ """
+ # Prepare
+ from codecarbon.core.units import Energy, Power, Time
+ from codecarbon.external.hardware import GPU
+
+ energy_consumption = {
+ "handle_0": [100_701, 180_001, 190_001],
+ "handle_1": [149_702, 180_002, 200_002],
+ }
+
+ def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
+ return energy_consumption[handle].pop(0)
+
+ gpu1_energy2 = Energy.from_millijoules(energy_consumption["handle_0"][1])
+ gpu1_energy3 = Energy.from_millijoules(energy_consumption["handle_0"][2])
+ gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][1])
+ gpu2_energy3 = Energy.from_millijoules(energy_consumption["handle_1"][2])
+
+ gpu2_power2 = Power.from_energies_and_delay(gpu1_energy2, gpu1_energy3, Time(5))
+ gpu1_power2 = Power.from_energies_and_delay(gpu2_energy2, gpu2_energy3, Time(5))
+ expected_power = gpu1_power2 + gpu2_power2
+
+ with mock.patch(
+ "pynvml.nvmlDeviceGetTotalEnergyConsumption",
+ side_effect=mock_nvmlDeviceGetTotalEnergyConsumption,
+ ):
+ gpu = GPU.from_utils()
+ gpu.measure_power_and_energy(5)
+
+ assert expected_power.kW == gpu.total_power().kW
+
+ def test_gpu_metadata_one_gpu_power(self):
+ """
+ Get the power of just one GPU even if there are more than 1
+ """
+ # Prepare
+ from codecarbon.core.units import Energy, Power, Time
+ from codecarbon.external.hardware import GPU
+
+ energy_consumption_mock = {
+ "handle_0": [100_701, 180_001, 190_001],
+ "handle_1": [149_702, 180_002, 200_002],
+ }
+ energy_consumption = deepcopy(energy_consumption_mock)
+
+ def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
+ return energy_consumption_mock[handle].pop(0)
+
+ with mock.patch(
+ "pynvml.nvmlDeviceGetTotalEnergyConsumption",
+ side_effect=mock_nvmlDeviceGetTotalEnergyConsumption,
+ ):
+ gpu = GPU.from_utils()
+ gpu.measure_power_and_energy(5, gpu_ids=[1])
+ print(energy_consumption)
+ gpu2_energy1 = Energy.from_millijoules(energy_consumption["handle_1"][1])
+ gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][2])
+ gpu2_power = Power.from_energies_and_delay(gpu2_energy1, gpu2_energy2, Time(5))
+ expected_power = gpu2_power
+
+ assert expected_power.kW == gpu.total_power().kW
+
+ @mock.patch.dict(
+ os.environ,
+ {
+ "CUDA_VISIBLE_DEVICES": "1",
+ },
+ )
+ def test_gpu_metadata_one_gpu_power_CUDA_VISIBLE_DEVICES(self):
+ """
+ Get the power of just one GPU even if there are more than 1
+ """
+ # Prepare
+ # (Note: This imports should be inside the test, not on top of the file, otherwise the mock does not work)
+ from codecarbon.core.units import Energy, Power, Time
+ from codecarbon.external.hardware import GPU
+
+ energy_consumption_mock = {
+ "handle_0": [100_000, 100_001, 100_002],
+ "handle_1": [149_702, 180_002, 200_002],
+ }
+ energy_consumption = deepcopy(energy_consumption_mock)
+
+ def mock_nvmlDeviceGetTotalEnergyConsumption(handle):
+ # print("mock_nvmlDeviceGetTotalEnergyConsumption", handle, energy_consumption_mock[handle])
+ return energy_consumption_mock[handle].pop(0)
+
+ # Call
+ with mock.patch(
+ "pynvml.nvmlDeviceGetTotalEnergyConsumption",
+ side_effect=mock_nvmlDeviceGetTotalEnergyConsumption, # Mock the energy consumption
+ ):
+ gpu = GPU.from_utils(gpu_ids=[int(os.environ["CUDA_VISIBLE_DEVICES"])])
+ # Despite the fact that there are 2 GPUs, only one is being used
+ assert gpu.gpu_ids == [1]
+ gpu.measure_power_and_energy(5)
+
+ # Assert
+ # ((200_002 - 180_002) * 10 ** (-3)) * 2.77778e-7 * 3_600 /5 = 0.0040000031999999994 kW
+ gpu2_energy1 = Energy.from_millijoules(energy_consumption["handle_1"][1])
+ gpu2_energy2 = Energy.from_millijoules(energy_consumption["handle_1"][2])
+ gpu2_power = Power.from_energies_and_delay(gpu2_energy1, gpu2_energy2, Time(5))
+ expected_power = gpu2_power
+ tc.assertAlmostEqual(expected_power.kW, gpu.total_power().kW)
+
+ def test_get_gpu_ids(self):
+ """
+ Check parsing of gpu_ids in various forms.
+ """
+ # Prepare
+ from codecarbon.external.hardware import GPU
+
+ for test_ids, expected_ids in [
+ ([0, 1], [0, 1]),
+ ([0, 1, 2], [0, 1]),
+ ([2], []),
+ (["0", "1"], [0, 1]),
+ # Only two GPUS in the system, so ignore the third (index 2)
+ (["0", "1", "2"], [0, 1]),
+ (["2"], []),
+ # Check UUID-to-index mapping
+ (["uuid-1"], [0]),
+ (["uuid-1", "uuid-2"], [0, 1]),
+ (["uuid-3"], []),
+ # Check UUID-to-index mapping when we need to strip the prefix
+ (["MIG-uuid-1"], [0]),
+ (["MIG-uuid-3"], []),
+ ]:
+ gpu = GPU(test_ids)
+ result = gpu._get_gpu_ids()
+ assert result == expected_ids
+
+
+class TestGpuNotAvailable:
+ def setup_method(self):
+ self.old_sys_path = copy(sys.path)
+ fake_module_path = os.path.join(os.path.dirname(__file__), "fake_modules")
+ sys.path.insert(0, fake_module_path)
+
+ # Clean old modules
+ try:
+ del sys.modules["pynvml"]
+ except KeyError:
+ pass
+
+ try:
+ del sys.modules["codecarbon.core.gpu"]
+ except KeyError:
+ pass
+ for module_name in (
+ "codecarbon.core.gpu_amd",
+ "codecarbon.core.gpu_nvidia",
+ "codecarbon.core.gpu_device",
+ ):
+ sys.modules.pop(module_name, None)
+
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+
+ import codecarbon.core
+
+ for attr in ["gpu", "gpu_nvidia", "gpu_amd", "gpu_device"]:
+ if hasattr(codecarbon.core, attr):
+ delattr(codecarbon.core, attr)
+
+ import pynvml
+
+ pynvml.INIT_MOCK.side_effect = pynvml.NVMLError("NVML Shared Library Not Found")
+
+ def teardown_method(self):
+ import pynvml
+
+ pynvml.INIT_MOCK.reset_mock()
+
+ # Restore the old paths
+ sys.path = self.old_sys_path
+
+ def test_is_gpu_details_not_available(self):
+ from codecarbon.core.gpu import is_gpu_details_available
+
+ assert is_gpu_details_available() is False
+
+ def test_gpu_details_not_available(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ alldevices = AllGPUDevices()
+
+ assert alldevices.get_gpu_details() == []
+
+ def test_static_gpu_info_not_available(self):
+ from codecarbon.core.gpu import AllGPUDevices
+
+ alldevices = AllGPUDevices()
+
+ assert alldevices.get_gpu_static_info() == []
diff --git a/uv.lock b/uv.lock
index 3b4a81200..92271e0ba 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,10 +2,22 @@ version = 1
revision = 2
requires-python = ">=3.10"
resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.11'",
- "python_full_version == '3.11.*'",
- "python_full_version >= '3.12' and python_full_version < '3.14'",
- "python_full_version >= '3.14'",
+]
+
+[[package]]
+name = "amdsmi"
+version = "7.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/c1/330da195623ec7d9f699be2dbec98df364b1def9b48aa169f1abe369804f/amdsmi-7.0.2.tar.gz", hash = "sha256:3e622e48c630b889045a6f57387455cdf082066348718172dd8af6d275baf8f2", size = 61577, upload-time = "2025-10-11T05:17:44.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/cf/bacc741d9926d662e76fd694f5bc63dd4c2471e32cedfb1c3cca7e47aa3c/amdsmi-7.0.2-py3-none-any.whl", hash = "sha256:db5aa757f8ed82dfd799c4d39e2828542678dc4e485b0ab7fabe5f398fec5652", size = 64366, upload-time = "2025-10-11T05:17:44.047Z" },
]
[[package]]
@@ -53,7 +65,7 @@ wheels = [
[[package]]
name = "black"
-version = "26.3.1"
+version = "26.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -65,34 +77,34 @@ dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" },
- { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" },
- { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" },
- { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" },
- { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" },
- { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" },
- { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" },
- { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" },
- { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" },
- { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" },
- { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" },
- { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" },
- { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" },
- { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" },
- { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" },
- { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" },
- { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" },
- { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" },
- { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" },
- { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" },
- { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" },
- { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" },
- { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" },
- { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" },
- { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" },
- { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/11/5f/25b7b149b8b7d3b958efa4faa56446560408c0f2651108a517526de0320a/black-26.3.0.tar.gz", hash = "sha256:4d438dfdba1c807c6c7c63c4f15794dda0820d2222e7c4105042ac9ddfc5dd0b", size = 664127, upload-time = "2026-03-06T17:42:33.7Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/45/0df73428226c2197b8b1e2ca15654f85cece1efe5f060c910b641a35de4a/black-26.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:135bf8a352e35b3bfba4999c256063d8d86514654599eca7635e914a55d60ec3", size = 1866623, upload-time = "2026-03-06T17:46:07.622Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e1/7467fcccf3532853b013bee22c9cdef6aa3314a58ccc73eb5a8a2750e50e/black-26.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6024a2959b6c62c311c564ce23ce0eaa977a50ed52a53f7abc83d2c9eb62b8d8", size = 1703733, upload-time = "2026-03-06T17:46:09.334Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/ceb0a5091b6dff654f77ee6488b91d45fbea1385338798935eb83090d27e/black-26.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:264144203ea3374542a1591b6fb317561662d074bce5d91ad6afa8d8d3e4ec3d", size = 1768094, upload-time = "2026-03-06T17:46:11.182Z" },
+ { url = "https://files.pythonhosted.org/packages/49/cc/6af7e15fb728f30f3e3d4257d2f3d3fe5c5f4ada30b0e8feb92f50118d5c/black-26.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:1a15d1386dce3af3993bf9baeb68d3e492cbb003dae05c3ecf8530a9b75edf85", size = 1413004, upload-time = "2026-03-06T17:46:12.867Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/04/7f5ffd40078ab54efa738797e1d547a3fce893f1de212a7a2e65b4a36254/black-26.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:d86a70bf048235aff62a79e229fe5d9e7809c7a05a3dd12982e7ccdc2678e096", size = 1219839, upload-time = "2026-03-06T17:46:14.133Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ec/e4db9f2b2db8226ae20d48b589c69fd64477657bf241c8ccaea3bc4feafa/black-26.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da07abe65732483e915ab7f9c7c50332c293056436e9519373775d62539607c", size = 1851905, upload-time = "2026-03-06T17:46:15.447Z" },
+ { url = "https://files.pythonhosted.org/packages/62/2c/ccecfcbd6a0610ecf554e852a146f053eaeb5b281dd9cb634338518c765e/black-26.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc9fd683ccabc3dc9791b93db494d93b5c6c03b105453b76d71e5474e9dfa6e7", size = 1689299, upload-time = "2026-03-06T17:46:17.396Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/53/8dcb860242012d6da9c6b1b930c3e4c947eb42feb1fc70f2a4e7332c90c5/black-26.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2c7e2c5ee09ff575869258b2c07064c952637918fc5e15f6ebd45e45eae0aa", size = 1753902, upload-time = "2026-03-06T17:46:19.592Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/21/f37b3efcc8cf2d01ec9eb5466598aa53bed2292db236723ac4571e24c4de/black-26.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:a849286bfc3054eaeb233b6df9056fcf969ee18bf7ecb71b0257e838a0f05e6d", size = 1413841, upload-time = "2026-03-06T17:46:20.981Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/74/e70f5f2a74301d8f10276b90715699d51d7db1c3dd79cf13966d32ba7b18/black-26.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:c93c83af43cda73ed8265d001214779ab245fa7a861a75b3e43828f4fb1f5657", size = 1220105, upload-time = "2026-03-06T17:46:23.269Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/76/b21711045b7f4c4f1774048d0b34dd10a265c42255658b251ce3303ae3c7/black-26.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2b1e5eec220b419e3591a0aaa6351bd3a9c01fe6291fbaf76d84308eb7a2ede", size = 1895944, upload-time = "2026-03-06T17:46:24.841Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c3/8c56e73283326bc92a36101c660228fff09a2403a57a03cacf3f7f84cf62/black-26.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bab64de70bccc992432bee56cdffbe004ceeaa07352127c386faa87e81f9261", size = 1718669, upload-time = "2026-03-06T17:46:26.639Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/8b/712a3ae8f17c1f3cd6f9ac2fffb167a27192f5c7aba68724e8c4ab8474ad/black-26.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b6c5f734290803b7b26493ffd734b02b72e6c90d82d45ac4d5b862b9bdf7720", size = 1794844, upload-time = "2026-03-06T17:46:28.334Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/5b/ee955040e446df86473287dd24dc69c80dd05e02cc358bca90e22059f7b1/black-26.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c767396af15b54e1a6aae99ddf241ae97e589f666b1d22c4b6618282a04e4ca", size = 1420461, upload-time = "2026-03-06T17:46:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/12/77/40b8bd44f032bb34c9ebf47ffc5bb47a2520d29e0a4b8a780ab515223b5a/black-26.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:765fd6ddd00f35c55250fdc6b790c272d54ac3f44da719cc42df428269b45980", size = 1229667, upload-time = "2026-03-06T17:46:31.654Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c3/21a834ce3de02c64221243f2adac63fa3c3f441efdb3adbf4136b33dfeb0/black-26.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59754fd8f43ef457be190594c07a52c999e22cb1534dc5344bff1d46fdf1027d", size = 1895195, upload-time = "2026-03-06T17:46:33.12Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f9/212d9697dd78362dadb778d4616b74c8c2cf7f2e4a55aac2adeb0576f2e9/black-26.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fd94cfee67b8d336761a0b08629a25938e4a491c440951ce517a7209c99b5ff", size = 1718472, upload-time = "2026-03-06T17:46:34.576Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/dd/da980b2f512441375b73cb511f38a2c3db4be83ccaa1302b8d39c9fa2dff/black-26.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b3e653a90ca1ef4e821c20f8edaee80b649c38d2532ed2e9073a9534b14a7", size = 1793741, upload-time = "2026-03-06T17:46:36.261Z" },
+ { url = "https://files.pythonhosted.org/packages/93/11/cd69ae8826fe3bc6eaf525c8c557266d522b258154a2968eb46d6d25fac7/black-26.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:f8fb9d7c2496adc83614856e1f6e55a9ce4b7ae7fc7f45b46af9189ddb493464", size = 1422522, upload-time = "2026-03-06T17:46:37.607Z" },
+ { url = "https://files.pythonhosted.org/packages/75/f5/647cf50255203eb286be197925e86eedc101d5409147505db3e463229228/black-26.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e8618c1d06838f56afbcb3ffa1aa16436cec62b86b38c7b32ca86f53948ffb91", size = 1231807, upload-time = "2026-03-06T17:46:39.072Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/77/b197e701f15fd694d20d8ee0001efa2e29eba917aa7c3610ff7b10ae0f88/black-26.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0c6f64ead44f4369c66f1339ecf68e99b40f2e44253c257f7807c5a3ef0ca32", size = 1889209, upload-time = "2026-03-06T17:46:40.453Z" },
+ { url = "https://files.pythonhosted.org/packages/93/85/b4d4924ac898adc2e39fc7a923bed99797535bc16dea4bc63944c3903c2b/black-26.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed6f0809134e51ec4a7509e069cdfa42bf996bd0fd1df6d3146b907f36e28893", size = 1720830, upload-time = "2026-03-06T17:46:42.009Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b1/5c0bf29fe5b43fcc6f3e8480c6566d21a02d4e702b3846944e7daa06dea9/black-26.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc6ac0ea5dd5fa6311ca82edfa3620cba0ed0426022d10d2d5d39aedbf3e1958", size = 1787676, upload-time = "2026-03-06T17:46:43.382Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ce/cc8cf14806c144d6a16512272c537d5450f50675d3e8c038705430e90fd9/black-26.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:884bc0aefa96adabcba0b77b10e9775fd52d4b766e88c44dc6f41f7c82787fc8", size = 1445406, upload-time = "2026-03-06T17:46:44.948Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/bb/049ea0fad9f8bdec7b647948adcf74bb720bd71dcb213decd553e05b2699/black-26.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:be3bd02aab5c4ab03703172f5530ddc8fc8b5b7bb8786230e84c9e011cee9ca1", size = 1257945, upload-time = "2026-03-06T17:46:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/39/d7/7360654ba4f8b41afcaeb5aca973cfea5591da75aff79b0a8ae0bb8883f6/black-26.3.0-py3-none-any.whl", hash = "sha256:e825d6b121910dff6f04d7691f826d2449327e8e71c26254c030c4f3d2311985", size = 206848, upload-time = "2026-03-06T17:42:31.133Z" },
]
[[package]]
@@ -328,7 +340,8 @@ dependencies = [
{ name = "authlib" },
{ name = "click" },
{ name = "nvidia-ml-py" },
- { name = "pandas" },
+ { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "prometheus-client" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
@@ -342,6 +355,9 @@ dependencies = [
]
[package.optional-dependencies]
+amdsmi = [
+ { name = "amdsmi" },
+]
carbonboard = [
{ name = "dash" },
{ name = "dash-bootstrap-components" },
@@ -377,6 +393,7 @@ doc = [
[package.metadata]
requires-dist = [
+ { name = "amdsmi", marker = "extra == 'amdsmi'", specifier = ">=6.0.0" },
{ name = "arrow" },
{ name = "authlib", specifier = ">=1.2.1" },
{ name = "click" },
@@ -400,7 +417,7 @@ requires-dist = [
{ name = "rich" },
{ name = "typer" },
]
-provides-extras = ["carbonboard", "viz-legacy"]
+provides-extras = ["carbonboard", "viz-legacy", "amdsmi"]
[package.metadata.requires-dev]
dev = [
@@ -734,14 +751,14 @@ wheels = [
[[package]]
name = "googleapis-common-protos"
-version = "1.72.0"
+version = "1.73.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
+ { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" },
]
[[package]]
@@ -917,7 +934,7 @@ wheels = [
[[package]]
name = "logfire"
-version = "4.26.0"
+version = "4.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "executing" },
@@ -929,9 +946,9 @@ dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a7/84/303c790f51dc96e4bca7c91025a163ad9413857eea6726230d5dc4e8de07/logfire-4.26.0.tar.gz", hash = "sha256:ea9b5fb6ca89c460f391ffc8f23392b5ecb38ea8426c08845cca8ba8a1eb5c85", size = 1055710, upload-time = "2026-03-06T09:29:30.757Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/5a/2ce2764fab3a23f1b49a799c1a5ab759c3e8200be300c108755ef5e8b73c/logfire-4.27.0.tar.gz", hash = "sha256:d05366abc4a16acb44b62dc9ca68933591a9755e138fc3e072cb73813db10d45", size = 1055824, upload-time = "2026-03-06T18:24:28.041Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d3/ef/83194d1d6a49f7b2cca843978d6ae29cc5b70f6c77a638443456b4fe848a/logfire-4.26.0-py3-none-any.whl", hash = "sha256:ff8a13ac47542f6280dc847826f78f7e11122057c16d59667344a9e2041fa6e8", size = 301127, upload-time = "2026-03-06T09:29:27.558Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b1/24eca9932376df4fba6902394c03532a138e87478a5a810799f0a22217a5/logfire-4.27.0-py3-none-any.whl", hash = "sha256:c1db6357d59ed4d58d614bdc9a90fcc46ddb7d1a7410e2bd56fa441e7c61f4e4", size = 301261, upload-time = "2026-03-06T18:24:25.101Z" },
]
[[package]]
@@ -1166,6 +1183,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" },
]
+[[package]]
+name = "mslex"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" },
+]
+
[[package]]
name = "mypy"
version = "1.19.1"
@@ -1252,6 +1278,9 @@ wheels = [
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
@@ -1310,6 +1339,93 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
]
+[[package]]
+name = "numpy"
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" },
+ { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" },
+ { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" },
+ { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
+ { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
+ { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
+ { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
+ { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
+ { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
+ { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
+ { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
+ { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
+ { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
+ { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
+ { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
+ { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" },
+ { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" },
+ { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" },
+]
+
[[package]]
name = "nvidia-ml-py"
version = "13.590.48"
@@ -1429,11 +1545,14 @@ wheels = [
name = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
dependencies = [
- { name = "numpy" },
- { name = "python-dateutil" },
- { name = "pytz" },
- { name = "tzdata" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version < '3.11'" },
+ { name = "pytz", marker = "python_full_version < '3.11'" },
+ { name = "tzdata", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [
@@ -1486,6 +1605,74 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
]
+[[package]]
+name = "pandas"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version >= '3.11'" },
+ { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" },
+ { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" },
+ { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" },
+ { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" },
+ { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" },
+ { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" },
+ { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" },
+ { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" },
+ { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" },
+ { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" },
+ { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" },
+ { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" },
+ { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" },
+ { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" },
+ { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" },
+ { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" },
+]
+
[[package]]
name = "pathspec"
version = "1.0.4"
@@ -1580,30 +1767,17 @@ wheels = [
[[package]]
name = "psutil"
-version = "7.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
- { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
- { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
- { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
- { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
- { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
- { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
- { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
- { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
- { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
- { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
- { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
- { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
- { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
- { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
- { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
- { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
- { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
- { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
- { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
+version = "6.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" },
+ { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" },
+ { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" },
+ { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" },
]
[[package]]
@@ -1843,15 +2017,15 @@ wheels = [
[[package]]
name = "python-discovery"
-version = "1.1.0"
+version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/67/09765eacf4e44413c4f8943ba5a317fcb9c7b447c3b8b0b7fce7e3090b0b/python_discovery-1.1.1.tar.gz", hash = "sha256:584c08b141c5b7029f206b4e8b78b1a1764b22121e21519b89dec56936e95b0a", size = 56016, upload-time = "2026-03-07T00:00:56.354Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0f/2bf7e3b5a4a65f623cb820feb5793e243fad58ae561015ee15a6152f67a2/python_discovery-1.1.1-py3-none-any.whl", hash = "sha256:69f11073fa2392251e405d4e847d60ffffd25fd762a0dc4d1a7d6b9c3f79f1a3", size = 30732, upload-time = "2026-03-07T00:00:55.143Z" },
]
[[package]]
@@ -2197,14 +2371,17 @@ wheels = [
[[package]]
name = "taskipy"
-version = "1.2.1"
+version = "1.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "toml" },
+ { name = "colorama" },
+ { name = "mslex", marker = "sys_platform == 'win32'" },
+ { name = "psutil" },
+ { name = "tomli", marker = "python_full_version < '4'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/79/c4/8882cfa772ba065919f43622bdf6304558a29735230efecda1726871ac6c/taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175", size = 5862, upload-time = "2020-03-20T23:12:37.995Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475, upload-time = "2024-11-26T16:37:46.155Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/d0/87ce1d3f91f97a60d673f4ed721b51c730576ebe2061da3ba84e49dd6c7c/taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2", size = 5771, upload-time = "2020-03-20T23:12:36.499Z" },
+ { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052, upload-time = "2024-11-26T16:37:44.546Z" },
]
[[package]]