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
126 changes: 124 additions & 2 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,90 @@ def monitor(
str,
typer.Option(help="Log level (critical, error, warning, info, debug)"),
] = "error",
project_name: Annotated[
str | None, typer.Option(help="Project name for the monitored run")
] = None,
api_endpoint: Annotated[
str | None, typer.Option(help="CodeCarbon API endpoint")
] = None,
api_key: Annotated[str | None, typer.Option(help="CodeCarbon API key")] = None,
output_dir: Annotated[
str | None, typer.Option(help="Directory for output files")
] = None,
output_file: Annotated[str | None, typer.Option(help="Output CSV filename")] = None,
output_methods: Annotated[
str | None,
typer.Option(help="Comma-separated output methods (csv, api, logger, ...)"),
] = None,
save_to_file: Annotated[
bool | None,
typer.Option(help="Deprecated: save emissions to a CSV file"),
] = None,
save_to_logger: Annotated[
bool | None,
typer.Option(help="Deprecated: write emissions to a logger"),
] = None,
save_to_prometheus: Annotated[
bool | None,
typer.Option(help="Deprecated: push emissions to Prometheus"),
] = None,
save_to_logfire: Annotated[
bool | None,
typer.Option(help="Deprecated: write emissions to Logfire"),
] = None,
prometheus_url: Annotated[
str | None, typer.Option(help="Prometheus server URL")
] = None,
gpu_ids: Annotated[
str | None, typer.Option(help="Comma-separated GPU IDs to track")
] = None,
emissions_endpoint: Annotated[
str | None, typer.Option(help="HTTP endpoint for emissions data")
] = None,
experiment_id: Annotated[str | None, typer.Option(help="Experiment ID")] = None,
experiment_name: Annotated[str | None, typer.Option(help="Experiment name")] = None,
electricitymaps_api_token: Annotated[
str | None, typer.Option(help="Electricity Maps API token")
] = None,
co2_signal_api_token: Annotated[
str | None,
typer.Option(help="Deprecated: use --electricitymaps-api-token"),
] = None,
tracking_mode: Annotated[
str | None, typer.Option(help="Tracking mode: process or machine")
] = None,
on_csv_write: Annotated[
str | None, typer.Option(help="CSV write mode: append or update")
] = None,
logger_preamble: Annotated[
str | None, typer.Option(help="Text prefixed to tracker log messages")
] = None,
force_cpu_power: Annotated[
int | None, typer.Option(help="Override CPU power in watts")
] = None,
force_ram_power: Annotated[
int | None, typer.Option(help="Override RAM power in watts")
] = None,
pue: Annotated[float | None, typer.Option(help="Power Usage Effectiveness")] = None,
wue: Annotated[
float | None, typer.Option(help="Water Usage Effectiveness in L/kWh")
] = None,
force_carbon_intensity_g_co2e_kwh: Annotated[
float | None,
typer.Option(help="Override carbon intensity in gCO2e/kWh"),
] = None,
force_mode_cpu_load: Annotated[
bool | None, typer.Option(help="Force CPU load estimation mode")
] = None,
allow_multiple_runs: Annotated[
bool | None, typer.Option(help="Allow concurrent CodeCarbon instances")
] = None,
rapl_include_dram: Annotated[
bool | None, typer.Option(help="Include DRAM power in RAPL measurements")
] = None,
rapl_prefer_psys: Annotated[
bool | None, typer.Option(help="Prefer the RAPL platform power domain")
] = None,
):
"""Monitor your machine's carbon emissions."""

Expand All @@ -399,6 +483,44 @@ def monitor(
"api_call_interval": api_call_interval,
"log_level": log_level,
}
optional_tracker_args = {
"project_name": project_name,
"api_endpoint": api_endpoint,
"api_key": api_key,
"output_dir": output_dir,
"output_file": output_file,
"output_methods": output_methods,
"save_to_file": save_to_file,
"save_to_logger": save_to_logger,
"save_to_prometheus": save_to_prometheus,
"save_to_logfire": save_to_logfire,
"prometheus_url": prometheus_url,
"gpu_ids": gpu_ids,
"emissions_endpoint": emissions_endpoint,
"experiment_id": experiment_id,
"experiment_name": experiment_name,
"electricitymaps_api_token": electricitymaps_api_token,
"co2_signal_api_token": co2_signal_api_token,
"tracking_mode": tracking_mode,
"on_csv_write": on_csv_write,
"logger_preamble": logger_preamble,
"force_cpu_power": force_cpu_power,
"force_ram_power": force_ram_power,
"pue": pue,
"wue": wue,
"force_carbon_intensity_g_co2e_kwh": force_carbon_intensity_g_co2e_kwh,
"force_mode_cpu_load": force_mode_cpu_load,
"allow_multiple_runs": allow_multiple_runs,
"rapl_include_dram": rapl_include_dram,
"rapl_prefer_psys": rapl_prefer_psys,
}
tracker_args.update(
{
name: value
for name, value in optional_tracker_args.items()
if value is not None
}
)
# Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode
if offline:
if not country_iso_code:
Expand All @@ -414,8 +536,8 @@ def monitor(
"region": region,
}
else:
experiment_id = get_existing_exp_id()
if api and experiment_id is None:
configured_experiment_id = experiment_id or get_existing_exp_id()
if api and configured_experiment_id is None:
print(
"ERROR: No experiment id. Set CODECARBON_EXPERIMENT_ID, call 'codecarbon config' first, or run in offline mode with `--offline --country-iso-code FRA`.",
file=sys.stderr,
Expand Down
171 changes: 171 additions & 0 deletions tests/cli/test_cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,177 @@ def stop(self):
assert calls["kwargs"]["region"] == "IDF"


def test_monitor_forwards_explicit_tracker_options(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["args"] = list(ctx.args)
captured["offline"] = offline
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)

runner = CliRunner()
result = runner.invoke(
cli_main.codecarbon,
[
"monitor",
"--offline",
"--country-iso-code",
"FRA",
"--project-name",
"cli-project",
"--api-endpoint",
"https://api.example.com",
"--api-key",
"secret",
"--output-dir",
"/tmp/emissions",
"--output-file",
"custom.csv",
"--output-methods",
"csv,logger",
"--no-save-to-file",
"--save-to-logger",
"--save-to-prometheus",
"--no-save-to-logfire",
"--prometheus-url",
"http://localhost:9091",
"--gpu-ids",
"0,2",
"--emissions-endpoint",
"https://emissions.example.com",
"--experiment-id",
"exp-123",
"--experiment-name",
"cli-experiment",
"--electricitymaps-api-token",
"electricity-token",
"--co2-signal-api-token",
"legacy-token",
"--tracking-mode",
"process",
"--on-csv-write",
"update",
"--logger-preamble",
"monitor:",
"--force-cpu-power",
"75",
"--force-ram-power",
"20",
"--pue",
"1.25",
"--wue",
"0.4",
"--force-carbon-intensity-g-co2e-kwh",
"42.5",
"--force-mode-cpu-load",
"--no-allow-multiple-runs",
"--rapl-include-dram",
"--no-rapl-prefer-psys",
"--",
"python",
"train.py",
],
)

assert result.exit_code == 0
assert captured["args"] == ["python", "train.py"]
assert captured["offline"] is True
assert captured["kwargs"] == {
"measure_power_secs": 10,
"api_call_interval": 30,
"log_level": "error",
"project_name": "cli-project",
"api_endpoint": "https://api.example.com",
"api_key": "secret",
"output_dir": "/tmp/emissions",
"output_file": "custom.csv",
"output_methods": "csv,logger",
"save_to_file": False,
"save_to_logger": True,
"save_to_prometheus": True,
"save_to_logfire": False,
"prometheus_url": "http://localhost:9091",
"gpu_ids": "0,2",
"emissions_endpoint": "https://emissions.example.com",
"experiment_id": "exp-123",
"experiment_name": "cli-experiment",
"electricitymaps_api_token": "electricity-token",
"co2_signal_api_token": "legacy-token",
"tracking_mode": "process",
"on_csv_write": "update",
"logger_preamble": "monitor:",
"force_cpu_power": 75,
"force_ram_power": 20,
"pue": 1.25,
"wue": 0.4,
"force_carbon_intensity_g_co2e_kwh": 42.5,
"force_mode_cpu_load": True,
"allow_multiple_runs": False,
"rapl_include_dram": True,
"rapl_prefer_psys": False,
"country_iso_code": "FRA",
"region": None,
}


def test_monitor_omits_unspecified_optional_tracker_options(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)

ctx = SimpleNamespace(args=["python", "train.py"])
cli_main.monitor(ctx=ctx, offline=True, country_iso_code="FRA")

assert "pue" not in captured["kwargs"]
assert "allow_multiple_runs" not in captured["kwargs"]
assert "output_methods" not in captured["kwargs"]


def test_monitor_explicit_experiment_id_satisfies_online_validation(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)
monkeypatch.setattr(cli_main, "get_existing_exp_id", lambda: None)

runner = CliRunner()
result = runner.invoke(
cli_main.codecarbon,
[
"monitor",
"--experiment-id",
"exp-from-cli",
"--",
"python",
"train.py",
],
)

assert result.exit_code == 0
assert captured["kwargs"]["experiment_id"] == "exp-from-cli"


def test_monitor_help_lists_explicit_tracker_options():
runner = CliRunner()
result = runner.invoke(
cli_main.codecarbon, ["monitor", "--help"], terminal_width=200
)

assert result.exit_code == 0
assert "--output-methods" in result.output
assert "--pue" in result.output
assert "--wue" in result.output
assert "--force-cpu-power" in result.output
assert "--rapl-include-dram" in result.output


def test_monitor_delegates_offline_flag_to_run_and_monitor(monkeypatch):
captured = {}

Expand Down