Skip to content

Commit 7a78cd8

Browse files
committed
Log computer load when we report CPU misconfiguration
The current load on the machine is relevant when interpreting the actual overspent seconds (but you will have to obtain the number of CPU-cores on the particular compute node elsewhere). (cherry picked from commit 7c75f0c)
1 parent b803afb commit 7a78cd8

8 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/_ert/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class ForwardModelStepRunning(ForwardModelStepBaseEvent):
8282
max_memory_usage: int | None = None
8383
current_memory_usage: int | None = None
8484
cpu_seconds: float = 0.0
85+
computer_load: float | None = None
8586

8687

8788
class ForwardModelStepSuccess(ForwardModelStepBaseEvent):

src/_ert/forward_model_runner/forward_model_step.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
from subprocess import Popen, run
1818
from typing import TYPE_CHECKING
1919

20-
from psutil import AccessDenied, NoSuchProcess, Process, TimeoutExpired, ZombieProcess
20+
from psutil import (
21+
AccessDenied,
22+
NoSuchProcess,
23+
Process,
24+
TimeoutExpired,
25+
ZombieProcess,
26+
getloadavg,
27+
)
2128

2229
from .reporting.message import (
2330
Exited,
@@ -215,6 +222,8 @@ def _run(self) -> Generator[Start | Exited | Running]:
215222
max_memory_usage = 0
216223
fm_step_pids = {int(process.pid)}
217224
cpu_seconds_processtree: ProcesstreeTimer = ProcesstreeTimer()
225+
computer_load_1min_sum = 0.0
226+
computer_load_1min_samples = 0
218227
while True:
219228
try:
220229
exit_code = process.wait(timeout=self.MEMORY_POLL_PERIOD)
@@ -238,6 +247,9 @@ def _run(self) -> Generator[Start | Exited | Running]:
238247
cpu_seconds_processtree.update(cpu_seconds_snapshot)
239248
fm_step_pids |= pids
240249
max_memory_usage = max(memory_rss, max_memory_usage)
250+
with contextlib.suppress(OSError):
251+
computer_load_1min_sum += getloadavg()[0]
252+
computer_load_1min_samples += 1
241253
yield Running(
242254
self,
243255
ProcessTreeStatus(
@@ -246,6 +258,11 @@ def _run(self) -> Generator[Start | Exited | Running]:
246258
fm_step_id=self.index,
247259
fm_step_name=self.step_data.get("name"),
248260
cpu_seconds=cpu_seconds_processtree.total_cpu_seconds(),
261+
computer_load=(
262+
computer_load_1min_sum / computer_load_1min_samples
263+
if computer_load_1min_samples
264+
else None
265+
),
249266
oom_score=oom_score,
250267
),
251268
)

src/_ert/forward_model_runner/reporting/event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def _step_handler(self, msg: Start | Running | Exited) -> None:
256256
max_memory_usage=msg.memory_status.max_rss,
257257
current_memory_usage=msg.memory_status.rss,
258258
cpu_seconds=msg.memory_status.cpu_seconds,
259+
computer_load=msg.memory_status.computer_load,
259260
)
260261
)
261262

src/_ert/forward_model_runner/reporting/message.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class ProcessTreeStatus:
3838

3939
cpu_seconds: float = 0.0
4040

41+
computer_load: float | None = None # Accumulated and averaged over time
42+
4143
oom_score: int | None = None
4244

4345
def __post_init__(self) -> None:

src/ert/ensemble_evaluator/evaluator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,11 +720,13 @@ def detect_overspent_cpu(
720720
parallelization_obtained > allowed_overspending
721721
and self.ensemble.queue_system != QueueSystem.LOCAL
722722
):
723+
computer_load = fm_step.get(ids.COMPUTER_LOAD, -1.0)
723724
logger.warning(
724725
f"Misconfigured NUM_CPU, forward model step '{fm_step.get(ids.NAME)}' "
725726
f"for realization {real_id} spent {cpu_seconds} cpu seconds "
726727
f"with wall clock duration {duration:.1f} seconds, a factor of "
727-
f"{parallelization_obtained:.2f}, while NUM_CPU was {num_cpu}."
728+
f"{parallelization_obtained:.2f}, while NUM_CPU was {num_cpu}, "
729+
f"computer load {computer_load:.2f}"
728730
)
729731
if parallelization_obtained > overspending_warning_threshold:
730732
warning_msg = (

src/ert/ensemble_evaluator/identifiers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
INDEX: Final = "index"
77
MAX_MEMORY_USAGE: Final = "max_memory_usage"
88
CPU_SECONDS: Final = "cpu_seconds"
9+
COMPUTER_LOAD: Final = "computer_load"
910
NAME: Final = "name"
1011
START_TIME: Final = "start_time"
1112
STATUS: Final = "status"

src/ert/ensemble_evaluator/snapshot.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ def update_from_realization_event(
354354
current_memory_usage=None,
355355
max_memory_usage=None,
356356
cpu_seconds=None,
357+
computer_load=None,
357358
error=None,
358359
stdout=None,
359360
stderr=None,
@@ -385,6 +386,7 @@ def update_from_fm_event(
385386
fm_data["current_memory_usage"] = event.current_memory_usage
386387
fm_data["max_memory_usage"] = event.max_memory_usage
387388
fm_data["cpu_seconds"] = event.cpu_seconds
389+
fm_data["computer_load"] = event.computer_load
388390
case ForwardModelStepSuccess():
389391
end_time = convert_iso8601_to_datetime(timestamp)
390392
# Make sure error msg from previous failed run is replaced
@@ -459,6 +461,7 @@ class FMStepSnapshot(TypedDict, total=False):
459461
current_memory_usage: int | None
460462
max_memory_usage: int | None
461463
cpu_seconds: float | None
464+
computer_load: float | None
462465
name: str | None
463466
error: str | None
464467
stdout: str | None

tests/ert/unit_tests/ensemble_evaluator/test_ensemble_evaluator.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,36 @@ def test_overspent_cpu_is_logged(
342342
caplog.clear()
343343

344344

345+
def test_that_average_computer_load_is_included_in_overspent_cpu_message(
346+
evaluator_to_use,
347+
caplog,
348+
monkeypatch,
349+
):
350+
caplog.set_level(logging.WARNING)
351+
evaluator, _ = evaluator_to_use
352+
353+
monkeypatch.setattr(TestEnsemble, "queue_system", QueueSystem.LSF)
354+
355+
start = datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC)
356+
duration = 100
357+
num_cpu = 1
358+
cpu_seconds = 1000.0 # Guarantees overspending is detected
359+
360+
evaluator.detect_overspent_cpu(
361+
num_cpu,
362+
"dummy",
363+
FMStepSnapshot(
364+
start_time=start,
365+
end_time=start + datetime.timedelta(seconds=duration),
366+
cpu_seconds=cpu_seconds,
367+
computer_load=12.34,
368+
),
369+
)
370+
371+
assert "Misconfigured NUM_CPU" in caplog.text
372+
assert "computer load 12.34" in caplog.text
373+
374+
345375
@pytest.mark.slow
346376
async def test_snapshot_on_resubmit_is_cleared(evaluator_to_use):
347377
(evaluator, event_queue) = evaluator_to_use

0 commit comments

Comments
 (0)