| ############################################################################ | |
| # 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 | |
| # Since 26.11, sacct shows the name of the user instead of the uid. | |
| # Remove this when 26.05 is no longer supported. | |
| expected = "CANCELLED by root" | |
| if atf.get_version("bin/sacct") < (26, 11): | |
| expected = "CANCELLED by 0" | |
| assert state == expected, ( | |
| f"Expected job to be cancelled by the daemon (root) via OverMemoryKill, " | |
| f"got: {state!r}" | |
| ) |