Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 67 additions & 7 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ def __init__(
tracking_mode: Optional[str] = _sentinel,
log_level: Optional[Union[int, str]] = _sentinel,
on_csv_write: Optional[str] = _sentinel,
csv_run_name: Optional[str] = _sentinel,
logger_preamble: Optional[str] = _sentinel,
force_cpu_power: Optional[int] = _sentinel,
force_ram_power: Optional[int] = _sentinel,
Expand Down Expand Up @@ -492,6 +493,11 @@ def __init__(
:param on_csv_write: When calling tracker.flush() manually: "update" to overwrite
the existing run_id row, or "append" to add a new row to the
CSV file. Defaults to "append".
:param csv_run_name: Optional CSV filename for interval measurement export.
When set, CodeCarbon appends a row on the same cadence as
API/Prometheus (``api_call_interval * measure_power_secs``).
Use ``"auto"`` (or an empty string) to name the file
``emissions_<run_id>.csv``. Defaults to None (disabled).
:param logger_preamble: String to systematically include in the logger.
messages. Defaults to "".
:param force_cpu_power: Force the CPU max power consumption in watts. Use this if you
Expand Down Expand Up @@ -578,6 +584,7 @@ def __init__(
self._set_from_conf(output_handlers, "output_handlers", [])
self._set_from_conf(tracking_mode, "tracking_mode", "machine")
self._set_from_conf(on_csv_write, "on_csv_write", "append")
self._set_from_conf(csv_run_name, "csv_run_name", None)
self._set_from_conf(logger_preamble, "logger_preamble", "")
self._set_from_conf(force_cpu_power, "force_cpu_power", None, float)
self._set_from_conf(force_ram_power, "force_ram_power", None, float)
Expand Down Expand Up @@ -609,7 +616,7 @@ def _init_output_methods(self, *, api_key: str = None):
"""
methods = set(self._output_methods) if self._output_methods else set()

if not methods and not self._emissions_endpoint:
if not methods and not self._emissions_endpoint and self._csv_run_name is None:
self.run_id = uuid.uuid4()
return

Expand All @@ -620,15 +627,15 @@ def _init_output_methods(self, *, api_key: str = None):
from codecarbon.output_methods.metrics.prometheus import PrometheusOutput

methods = set(self._output_methods) if self._output_methods else set()
csv_file_handler = None

if OutputMethod.CSV in methods:
self._output_handlers.append(
FileOutput(
self._output_file,
self._output_dir,
self._on_csv_write,
)
csv_file_handler = FileOutput(
self._output_file,
self._output_dir,
self._on_csv_write,
)
self._output_handlers.append(csv_file_handler)

if OutputMethod.LOGGER in methods:
self._output_handlers.append(self._logging_logger)
Expand Down Expand Up @@ -666,6 +673,54 @@ def _init_output_methods(self, *, api_key: str = None):
if OutputMethod.BOAMPS in methods:
self._output_handlers.append(BoAmpsOutput(output_dir=self._output_dir))

self._init_interval_csv_output(csv_file_handler)

def _resolve_csv_run_filename(self) -> Optional[str]:
"""Return the interval CSV filename, or None when interval export is disabled."""
if self._csv_run_name is None:
return None
name = self._csv_run_name.strip() if isinstance(self._csv_run_name, str) else ""
if name in ("", "auto"):
return f"emissions_{self.run_id}.csv"
return name

def _init_interval_csv_output(self, csv_file_handler) -> None:
"""
Opt-in interval CSV export (#467): write measurement rows on the same
cadence as API/Prometheus live outputs.
"""
from codecarbon.output_methods.file import FileOutput

interval_file_name = self._resolve_csv_run_filename()
if interval_file_name is None:
return

if (
csv_file_handler is not None
and csv_file_handler.output_file_name == interval_file_name
):
csv_file_handler.enable_live_out = True
logger.info(
"Interval CSV export enabled on %s "
"(every api_call_interval * measure_power_secs)",
interval_file_name,
)
return

self._output_handlers.append(
FileOutput(
interval_file_name,
self._output_dir,
on_csv_write="append",
enable_live_out=True,
)
)
logger.info(
"Interval CSV export enabled: %s "
"(every api_call_interval * measure_power_secs)",
interval_file_name,
)

def get_detected_hardware(self) -> Dict[str, Any]:
"""
Get the detected hardware.
Expand Down Expand Up @@ -1495,6 +1550,7 @@ def track_emissions(
tracking_mode: Optional[str] = _sentinel,
log_level: Optional[Union[int, str]] = _sentinel,
on_csv_write: Optional[str] = _sentinel,
csv_run_name: Optional[str] = _sentinel,
logger_preamble: Optional[str] = _sentinel,
offline: Optional[bool] = _sentinel,
country_iso_code: Optional[str] = _sentinel,
Expand Down Expand Up @@ -1555,6 +1611,8 @@ def track_emissions(
Defaults to "info".
:param on_csv_write: When calling tracker.flush() manually: "update" to overwrite the
existing run_id row, or "append" to add a new row. Defaults to "append".
:param csv_run_name: Optional CSV filename for interval measurement export. See
EmissionsTracker. Defaults to None (disabled).
:param logger_preamble: String to systematically include in the logger.
messages. Defaults to "".
:param allow_multiple_runs: Allow multiple CodeCarbon instances on the same machine.
Expand Down Expand Up @@ -1633,6 +1691,7 @@ def wrapped_fn(*args, **kwargs):
tracking_mode=tracking_mode,
log_level=log_level,
on_csv_write=on_csv_write,
csv_run_name=csv_run_name,
logger_preamble=logger_preamble,
country_iso_code=country_iso_code,
region=region,
Expand Down Expand Up @@ -1674,6 +1733,7 @@ def wrapped_fn(*args, **kwargs):
tracking_mode=tracking_mode,
log_level=log_level,
on_csv_write=on_csv_write,
csv_run_name=csv_run_name,
logger_preamble=logger_preamble,
force_cpu_power=force_cpu_power,
force_ram_power=force_ram_power,
Expand Down
14 changes: 13 additions & 1 deletion codecarbon/output_methods/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ class FileOutput(BaseOutput):
"""

def __init__(
self, output_file_name: str, output_dir: str, on_csv_write: str = "append"
self,
output_file_name: str,
output_dir: str,
on_csv_write: str = "append",
enable_live_out: bool = False,
):
"""
Initialize the FileOutput object.
Expand All @@ -31,6 +35,8 @@ def __init__(
output_file_name: name of file to write to.
output_dir: path to directory to write to.
on_csv_write: "append" or "update", whether or not to append or overwrite a file if it exists
enable_live_out: when True, also write on live measurement intervals
(same cadence as API/Prometheus: ``api_call_interval * measure_power_secs``).

Raises:
ValueError: If the on_csv_write value is invalid.
Expand All @@ -46,6 +52,7 @@ def __init__(
raise OSError(f"Folder '{output_dir}' doesn't exist !")
self.output_dir: str = output_dir
self.on_csv_write: str = on_csv_write
self.enable_live_out: bool = enable_live_out
self.save_file_path = os.path.join(self.output_dir, self.output_file_name)
logger.info(
f"Emissions data (if any) will be saved to file {os.path.abspath(self.save_file_path)}"
Expand All @@ -69,6 +76,11 @@ def has_valid_headers(self, data: EmissionsData) -> bool:
return True
return sorted(headers) == sorted(data.values.keys())

def live_out(self, total: EmissionsData, delta: EmissionsData):
"""Write a measurement row on the live interval when enabled."""
if self.enable_live_out:
self.out(total, delta)

def out(self, total: EmissionsData, _):
"""
Save the emissions data from a whole run to a CSV file.
Expand Down
12 changes: 12 additions & 0 deletions docs/reference/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ It can also be set in the config file as a comma-separated string, e.g.

The package has an in-built logger that logs data into a CSV file named `emissions.csv` in the `output_dir`, provided as an input parameter (defaults to the current directory), for each experiment tracked across projects.

By default that file is written once at the end of the run. To also append a row on the same live cadence as API/Prometheus (`api_call_interval × measure_power_secs`), set `csv_run_name`:

```python-skip
from codecarbon import EmissionsTracker

tracker = EmissionsTracker(
csv_run_name="emissions_live.csv", # or "auto" / "" → emissions_<run_id>.csv
)
```

Use the same name as `output_file` to enable live rows on the primary CSV. Leave `csv_run_name` unset to keep the default final-only behavior.

| Field | Description |
|-------|-------------|
| timestamp | Time of the experiment in `%Y-%m-%dT%H:%M:%S` format |
Expand Down
19 changes: 19 additions & 0 deletions tests/output_methods/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,22 @@ def test_out_append_large_file_fast_path(self):

df = pd.read_csv(os.path.join(self.temp_dir, "test.csv"))
self.assertEqual(len(df), 2)

def test_live_out_noop_when_disabled(self):
file_output = FileOutput("test.csv", self.temp_dir, on_csv_write="append")
file_output.live_out(self.emissions_data, self.emissions_data)
self.assertFalse(os.path.isfile(file_output.save_file_path))

def test_live_out_appends_when_enabled(self):
file_output = FileOutput(
"interval.csv",
self.temp_dir,
on_csv_write="append",
enable_live_out=True,
)
file_output.live_out(self.emissions_data, self.emissions_data)
file_output.live_out(self.emissions_data, self.emissions_data)

df = pd.read_csv(file_output.save_file_path)
self.assertEqual(len(df), 2)
self.assertEqual(df.iloc[0]["run_id"], "test_run_id")
Loading