Skip to content

Commit bceb640

Browse files
fix(slurm): warn only on non-zero SLURM_LOCALID, read step tasks-per-node
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent dae06ff commit bceb640

2 files changed

Lines changed: 42 additions & 22 deletions

File tree

‎codecarbon/core/slurm.py‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
import os
2-
import re
32

43
from codecarbon.external.logger import logger
54

65

76
def warn_on_multi_rank_double_counting(tracking_mode: str) -> None:
87
"""
9-
Warn when several ranks share a node and each one measures the whole node.
8+
Warn when a machine-mode tracker runs on a non-zero SLURM local rank.
109
11-
In ``machine`` mode every rank reports the node's power, so the job's
12-
reported footprint is silently multiplied by the number of ranks per node.
10+
In ``machine`` mode every tracker reports the whole node's power, so if
11+
more than one rank per node starts a tracker, the job's footprint is
12+
silently multiplied. Local rank 0 (the recommended tracker) and the batch
13+
step (``SLURM_LOCALID`` unset) never warn.
1314
"""
1415
if tracking_mode != "machine":
1516
return
16-
# SLURM writes this as "4", or as "4(x2)" for a heterogeneous allocation.
17-
ntasks_per_node = os.environ.get("SLURM_NTASKS_PER_NODE", "")
18-
match = re.match(r"\d+", ntasks_per_node)
19-
if match and int(match.group()) > 1:
20-
logger.warning(
21-
f"SLURM_NTASKS_PER_NODE is {ntasks_per_node} and tracking_mode is "
22-
"'machine': every rank measures the whole node, so the job's total "
23-
"will be multiplied by the number of ranks per node. Start the "
24-
"tracker on one rank per node (SLURM_LOCALID == 0), or use "
25-
"tracking_mode='process'."
26-
)
17+
local_id = os.environ.get("SLURM_LOCALID", "")
18+
if not local_id.isdigit() or int(local_id) == 0:
19+
return
20+
# Step-scoped and always set by srun; written "4", or "4(x2)".
21+
tasks_per_node = os.environ.get(
22+
"SLURM_STEP_TASKS_PER_NODE", os.environ.get("SLURM_TASKS_PER_NODE", "?")
23+
)
24+
logger.warning(
25+
f"tracking_mode is 'machine' on SLURM local rank {local_id} "
26+
f"(tasks per node: {tasks_per_node}). If more than one rank per node "
27+
"starts a tracker, each one measures the whole node and the job's total "
28+
"is multiplied. Start the tracker only on SLURM_LOCALID == 0, or use "
29+
"tracking_mode='process'."
30+
)

‎tests/test_slurm.py‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest import mock
33

44
from codecarbon.core.slurm import warn_on_multi_rank_double_counting
5+
from codecarbon.emissions_tracker import OfflineEmissionsTracker
56

67

78
class TestMultiRankWarning(unittest.TestCase):
@@ -11,14 +12,29 @@ def _warnings(self, tracking_mode="machine", **env):
1112
warn_on_multi_rank_double_counting(tracking_mode)
1213
return mocked_logger.warning.call_count
1314

14-
def test_several_ranks_per_node_in_machine_mode_warns(self):
15-
self.assertEqual(1, self._warnings(SLURM_NTASKS_PER_NODE="4"))
16-
# Heterogeneous allocations are written "4(x2)".
17-
self.assertEqual(1, self._warnings(SLURM_NTASKS_PER_NODE="4(x2)"))
15+
def test_non_zero_local_rank_in_machine_mode_warns(self):
16+
self.assertEqual(
17+
1, self._warnings(SLURM_LOCALID="1", SLURM_STEP_TASKS_PER_NODE="4")
18+
)
19+
# Heterogeneous allocations are written "4(x2)"; falls back to the job var.
20+
self.assertEqual(
21+
1, self._warnings(SLURM_LOCALID="3", SLURM_TASKS_PER_NODE="4(x2)")
22+
)
1823

1924
def test_no_warning_without_double_counting(self):
20-
self.assertEqual(0, self._warnings(SLURM_NTASKS_PER_NODE="1"))
21-
self.assertEqual(0, self._warnings())
25+
# Local rank 0 is the recommended tracker.
2226
self.assertEqual(
23-
0, self._warnings(tracking_mode="process", SLURM_NTASKS_PER_NODE="4")
27+
0, self._warnings(SLURM_LOCALID="0", SLURM_STEP_TASKS_PER_NODE="4")
2428
)
29+
# Batch step: per-node counts are exported but SLURM_LOCALID is not.
30+
self.assertEqual(
31+
0, self._warnings(SLURM_NTASKS_PER_NODE="4", SLURM_TASKS_PER_NODE="4")
32+
)
33+
self.assertEqual(0, self._warnings())
34+
self.assertEqual(0, self._warnings(tracking_mode="process", SLURM_LOCALID="1"))
35+
36+
def test_tracker_init_warns_on_non_zero_local_rank(self):
37+
with mock.patch.dict("os.environ", {"SLURM_LOCALID": "2"}):
38+
with mock.patch("codecarbon.core.slurm.logger") as mocked_logger:
39+
OfflineEmissionsTracker(country_iso_code="FRA", tracking_mode="machine")
40+
self.assertEqual(1, mocked_logger.warning.call_count)

0 commit comments

Comments
 (0)