blob: 0ea4530ea356584845ce5d77bfee8234e03aa6a9 [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test job_mem_limit_enforce() cancels an over-memory job via OverMemoryKill.
The combined memory usage of two overlapping steps exceeds the job memory
limit while neither step alone exceeds it, with
JobAcctGatherParams=OverMemoryKill configured.
"""
import pytest
import atf
job_mem_mib = 100
# Each step uses step_mib MiB; two steps combined (2 * step_mib) exceed
# job_mem_mib so the job is killed, but neither step alone exceeds it.
# This exercises the slurmd-level job_mem_limit_enforce() path rather than
# the per-step slurmstepd enforcement path.
step_mib = 55
node_real_mem_mib = 256
health_check_interval = 10
gather_freq_secs = 4
# Hold long enough for at least two HealthCheckInterval cycles plus one
# JobAcctGatherFrequency sample and step startup overhead.
hold_secs = 2 * health_check_interval + gather_freq_secs + 6
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 5),
"sbin/slurmd",
reason="Issue 51067: REQUEST_CANCEL_JOB_STEP memcpy fix available in 26.05+",
)
atf.require_config_parameter("SelectType", ["select/cons_tres", "select/linear"])
atf.require_config_parameter_includes(
"SelectTypeParameters", ["CR_Core_Memory", "CR_Memory"]
)
atf.require_config_parameter("JobAcctGatherType", "jobacct_gather/cgroup")
atf.require_config_parameter_includes("JobAcctGatherParams", "OverMemoryKill")
atf.require_config_parameter_includes(
"JobAcctGatherFrequency", ("task", gather_freq_secs)
)
# OverMemoryKill enforcement is documented to run on JobAcctGather sampling,
# but job-level detection also fires from job_mem_limit_enforce() on each
# health-check firing. This scaffolding is an implementation-timing crutch to
# detect the over-limit condition promptly; it is not documented behavior.
atf.require_config_parameter("HealthCheckInterval", health_check_interval)
atf.require_config_parameter("HealthCheckNodeState", "ANY")
atf.require_config_parameter("HealthCheckProgram", "/bin/true")
# Disable cgroup RAM and swap constraining so the kernel does not OOM-kill
# a step before job_mem_limit_enforce() can detect the over-limit condition
# and send REQUEST_CANCEL_JOB_STEP to the controller.
atf.require_config_parameter("ConstrainRAMSpace", "no", source="cgroup")
atf.require_config_parameter("ConstrainSwapSpace", "no", source="cgroup")
atf.require_nodes(1, [("RealMemory", node_real_mem_mib)])
atf.require_accounting()
atf.require_slurm_running()
def test_over_memory_kill(use_memory_program):
"""job_mem_limit_enforce() cancels a job when two overlapping steps
collectively exceed the job memory limit."""
job_id = atf.submit_job_sbatch(
f"--mem={job_mem_mib}M --wrap='"
f"srun --overlap {use_memory_program} {step_mib} {hold_secs} & "
f"srun --overlap {use_memory_program} {step_mib} {hold_secs} & "
f"wait'",
fatal=True,
)
for _ in atf.timer(fatal=True):
if (
atf.get_step_parameter(f"{job_id}.0", "State") == "RUNNING"
and atf.get_step_parameter(f"{job_id}.1", "State") == "RUNNING"
):
break
atf.wait_for_job_state(job_id, "CANCELLED", fatal=True)
# sacct reads slurmdbd, which is updated asynchronously from the slurmctld
# state that wait_for_job_state() polled, so retry until the record settles.
state = None
for _ in atf.timer(fatal=True):
state = atf.run_command_output(f"sacct -nP -X -j {job_id} -o state%30").strip()
if state and state != "RUNNING":
break
assert state == "CANCELLED by 0", (
f"Expected job to be cancelled by the daemon (uid 0) via OverMemoryKill, "
f"got: {state!r}"
)