Skip to content

feat(stack): read eb_hooks.py limits and a memory cap in stack oom - #14

Merged
HaoZeke merged 7 commits into
mainfrom
fix/oom-hooks-memory-cap
Nov 3, 2026
Merged

HaoZeke merged 7 commits into
mainfrom
fix/oom-hooks-memory-cap

Conversation

@HaoZeke

@HaoZeke HaoZeke commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Reproducer

From a checkout of this branch:

F=tests/oom_profile_fixtures
EASYSTACK=$F/easystacks/pr1772-eessi-2026.06-eb-5.4.0-2026.1.yml

eb-stack stack oom --hooks $F/hooks/integers.py
eb-stack stack oom --arch x86_64/amd/zen4 --hooks $F/hooks/divide-2.5.py --easystack $EASYSTACK
eb-stack stack oom --hooks $F/hooks/msg-plus.py
eb-stack stack oom --hooks $F/hooks/integers.py --parallel 12 --node 16:32768:x86_64/amd/zen4
eb-stack stack oom --arch x86_64/amd/zen4 --hooks $F/hooks/set-maximum-8.py --easystack $EASYSTACK
eb-stack stack oom --arch x86_64/amd/zen4 --hooks $F/hooks/divide-8.py --easystack $EASYSTACK

The --node 16:32768:x86_64/amd/zen4 value is one 16-core zen4 node with 32 GiB.
On main, stack oom has no --hooks, --parallel, --max-parallel or --mib-per-job option, so all six commands stop with a usage error.

What and why

Software-layer pull request 1772 lost three zen4 bot runs to a killed cc1 while building libxc, two at 8 processes and one at 5.
The run at 5 used a scripts commit that divides libxc by 3.
On main, stack oom reads its hook rows from a copy in data/eessi-bot-resources.toml.
Nobody can check a hook change with it before the change is merged.

  • The --hooks FILE option reads PARALLELISM_LIMITS from an eb_hooks.py and uses those rows in place of the copy.
    The file is parsed with rustpython-parser and never run.
    A float factor gives a warning with the file and line.
    An op the model cannot follow exits 1 with the line.
  • The same option warns on a statement that adds to a name and drops the sum, such as msg+ "..." in post_ready_hook.
    That half of the hook's log message never prints, and the fix belongs in software-layer-scripts.
  • Each --node line prints a memory cap beside the core cap for a package with an mib_per_job: min(current, memory_mib // mib_per_job).
    The --mib-per-job FILE option is an alias of --job-memory FILE.
  • The --parallel N and --max-parallel N options model a job that sets EasyBuild's settings.
    The hook still divides the node's cores, not --parallel.
    It keeps its result only when that result is lower.
  • The table in data/build-memory.toml records bot job 221273, which ran out at make -j 5.
    The zen4 figure for libxc goes from 4.0 to 6.4 GiB per process, still a lower bound.
  • The how-to checks two forms of libxc's row with --hooks: a cap of 8, (set_maximum, 8), and a reduction of 8, (divide_by_factor, 8).
    Both forms come from the review of software-layer-scripts pull request 337.
    The reduction of 8 is proposed there and not merged, and the current scripts divide libxc by 2.

Before and after

Each output here is quoted from the examples in docs/orgmode/howto/oom-profile.org on main or on this branch.

The default run, eb-stack stack oom --easystack $EASYSTACK, before:

libxc-7.0.0-GCC-15.2.0.eb may run out of memory on x86_64/amd/zen4 (eessi-bot-mc-aws amd-zen4): 8 x 4.0 GiB = 32.0 GiB, the job has 32.0 GiB (software-layer #1772)
  parallel 8: 16 cores, eb_hooks.py divides libxc by 2
  why: cc1 on the generated mgga_x_*.c sources; with GCC 15.2.0 eight compilers exceed 32 GiB on zen4
  remedy: maxparallel = 7 in libxc-7.0.0-GCC-15.2.0.eb, or a libxc limit of 7 for x86_64/amd/zen4 in PARALLELISM_LIMITS in software-layer-scripts eb_hooks.py; 4.0 GiB per process is a lower bound, so 7 may still be too many
  remedy: build x86_64/amd/zen4 on a node with more memory: eessi-bot-surf amd-zen4 (16 cores, 180.0 GiB, accel nvidia/cc90)
  remedy: the maintainers asked for a libxc divider of 2.5 or 3 in eb_hooks.py, which is 6 or 5 cores on the AWS build cluster
oom=1 fits=14 builds=5 nodes=15

After:

libxc-7.0.0-GCC-15.2.0.eb may run out of memory on x86_64/amd/zen4 (eessi-bot-mc-aws amd-zen4): 8 x 6.4 GiB = 51.2 GiB, the job has 32.0 GiB (software-layer #1772, software-layer-scripts #337)
  parallel 8: 16 cores, eb_hooks.py divides libxc by 2
  why: cc1 on the generated mgga_x_*.c sources; with GCC 15.2.0 on zen4, 8 and then 5 compilers ran out of a 32 GiB job
  remedy: maxparallel = 4 in libxc-7.0.0-GCC-15.2.0.eb, or a libxc limit of 4 for x86_64/amd/zen4 in PARALLELISM_LIMITS in software-layer-scripts eb_hooks.py; 6.4 GiB per process is a lower bound, so 4 may still be too many
  remedy: build x86_64/amd/zen4 on a node with more memory: eessi-bot-surf amd-zen4 (16 cores, 180.0 GiB, accel nvidia/cc90)
  remedy: a libxc divider of 3 left 5 cores and ran out too; a reduction of 8, as asked in the review of software-layer-scripts #337 (issuecomment-6599147327), is 2 cores on the AWS build cluster
oom=1 fits=14 builds=5 nodes=15

Before, resources-divide-2.5.toml fitted on the 32 GiB zen4 node, at 6 processes of 4.0 GiB:

oom=0 fits=1 builds=5 nodes=1

After, it warns at 6 processes of 6.4 GiB, 38.4 GiB in all:

oom=1 fits=0 builds=5 nodes=1

After, with --hooks on hooks/divide-2.5.py:

hooks: 3 PARALLELISM_LIMITS rows from tests/oom_profile_fixtures/hooks/divide-2.5.py
warning: tests/oom_profile_fixtures/hooks/divide-2.5.py:20: PARALLELISM_LIMITS divide_by_factor for libxc on * uses 2.5, which is not an integer; eb_hooks.py computes max(1, parallel // factor), and 16 // 2.5 is 6.0, a float. Use an integer factor.
libxc-7.0.0-GCC-15.2.0.eb may run out of memory on x86_64/amd/zen4 (eessi-bot-mc-aws amd-zen4): 6 x 6.4 GiB = 38.4 GiB, the job has 32.0 GiB (software-layer #1772, software-layer-scripts #337)
  parallel 6: 16 cores, eb_hooks.py divides libxc by 2.5 (16 // 2.5 is 6.0, a float)
  ...
oom=1 fits=1 builds=5 nodes=2

The dropped sum in hooks/msg-plus.py, after:

hooks: 3 PARALLELISM_LIMITS rows from tests/oom_profile_fixtures/hooks/msg-plus.py
warning: tests/oom_profile_fixtures/hooks/msg-plus.py:30: msg+ "to avoid out-of-memory failures during building/testing" in post_ready_hook adds to msg and drops the result, so msg does not change; msg += appends

The 16-core zen4 node with --parallel 12, after:

hooks: 3 PARALLELISM_LIMITS rows from tests/oom_profile_fixtures/hooks/integers.py
parallelism under the current eb_hooks.py limit and under limit_by_memory
node 16 cores, 32768 MiB, x86_64/amd/zen4, --parallel 12
  libxc: current 8 (divide_by_factor on *, max(1, 16 // 2)); memory unmeasured
job memory: libxc 7.0 unmeasured, no mib_per_job

The how-to also runs this node with --mib-per-job tests/oom_profile_fixtures/job-memory-fixture.toml.
That fixture gives libxc 8192 MiB as arithmetic, not as a measurement:

  libxc: current 8 (divide_by_factor on *, max(1, 16 // 2)); memory cap 4 (min(8, 32768 // 8192)); memory 4 (max(1, min(16, 32768 // 8192)))

The cap of 8 in hooks/set-maximum-8.py still warns on the 32 GiB zen4 node, and only the parallel line changes:

  parallel 8: 16 cores, eb_hooks.py caps libxc at 8
oom=1 fits=1 builds=5 nodes=2

The reduction of 8 in hooks/divide-8.py leaves 2 cores, so libxc fits on both zen4 nodes:

hooks: 3 PARALLELISM_LIMITS rows from tests/oom_profile_fixtures/hooks/divide-8.py
oom=0 fits=2 builds=5 nodes=2

Tests

A cloud VM ran the checks on an earlier copy of the 7 commits and the docs build on the last one.
Three conflicts in the docs and the changelog were resolved after that run.
CI on this pull request is the run on these commits.

Check Outcome on the earlier copy
cargo fmt --all --check pass
cargo clippy --locked --all-targets -- -D warnings pass
cargo test --locked --all-targets 1469 to 1484 passed, 0 failed, 10 ignored
python3 scripts/gen-llms-txt.py --check pass
sphinx-build -W --keep-going "build succeeded.", no warning

On the earlier copy, the 10 ignored tests need an EasyBuild tree or the network, and they're the same on every one of the 7 commits.
New tests cover the parsed hooks in src/eb_hooks.rs, the stack oom command in tests/oom_profile.rs, and the eb_stack_oom tool in tests/mcp_v1.rs.

Repro commands

cargo fmt --all --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all-targets
python3 scripts/gen-llms-txt.py --check
pixi run -e docs docdel && pixi run -e docs docbld

The last line runs scripts/sphinx-build-docs.sh, which calls python -m sphinx -W --keep-going.


Note

Medium Risk
Changes OOM advisory output and libxc memory bounds that drive hook/remedy suggestions; new Python parsing could misread exotic hook files, but behavior is check-only and heavily tested.

Overview
stack oom gains ways to preview hook and EasyBuild parallelism before bot jobs run, plus sharper libxc zen4 warnings after PR 1772’s failure at 5 processes.

--hooks FILE parses an EESSI eb_hooks.py (via new eb_hooks + rustpython-parser) for PARALLELISM_LIMITS instead of the bundled TOML copy; invalid rows exit 1 with a line number, non-integer divide factors and dropped msg+ sums warn. --parallel / --max-parallel model EasyBuild job settings while hooks still divide node cores and apply only when lower. --node (with --mib-per-job alias) prints a memory cap min(hook level, memory // mib_per_job) beside limit_by_memory. eb_stack_oom exposes the same flags and hook_warnings.

data/build-memory.toml raises libxc zen4 6.4 GiB/process (from 4.0), adds the job-221273 observation at parallel 5, and updates remedies (e.g. suggest maxparallel 4 and divide-by-8). Docs, CHANGELOG, and EESSI-extend skill describe the new flows and fixtures (set-maximum-8, divide-8, etc.).

Reviewed by Cursor Bugbot for commit e435f29. Bugbot is set up for automated code reviews on this repo. Configure here.

The `stack oom` command warned on a non-integer divide_by_factor only in the hook rows copied into `data/eessi-bot-resources.toml`.
The `--hooks FILE` option reads PARALLELISM_LIMITS from a software-layer-scripts eb_hooks.py with rustpython-parser, without running the file, and uses those rows in place of the table's.
A key may be a module-level string constant such as CPU_TARGET_A64FX, and a repeated key keeps its last value, as in Python.
A float factor is a warning that names the file and the line, since 16 // 2.5 is 6.0.
These exit 1 with the line: an op other than divide_by_factor or set_maximum, a `** unpacking`, and a non-string key.

The fixtures are `hooks/divide-2.5.py`, which warns, and `hooks/integers.py`, which does not and gives the bundled table's parallel levels.
The `eb_stack_oom` tool takes hooks.
In software-layer-scripts eb_hooks.py, the post_ready_hook builds its log line in two statements.
Since 8132905f the second is `msg+ "..."` on a line of its own.
That computes the longer string and drops it, so the "to avoid out-of-memory failures" half of the message never prints.
The `msg+` statement in eb_hooks.py drops its string; the fix belongs upstream.

With `--hooks`, the `stack oom` command now walks the parsed file.
It looks for an expression statement whose value is `name + ...`.
It also looks inside def, class, loop, if, with, match and try bodies.
It prints a warning with the file, the line, the statement and the enclosing function.
The form `msg += "..."` is an augmented assignment and is not reported.
The warning changes no parallel level and not the exit status.
The `eb_stack_oom` tool returns these as hook_warnings.

The fixtures `hooks/msg-plus.py` and `hooks/msg-plus-equals.py` are the `integers.py` rows with a post_ready_hook in each form.
On upstream software-layer-scripts main (43df6f4) the warning is eb_hooks.py:440.
The memory cap of a package on a node is the level the hook leaves, the core cap, capped at the number of compiles of mib_per_job MiB that fit in the node's memory: `min(core cap, memory // mib_per_job)`.
The `stack oom --node` command printed the hook's level and limit_by_memory in place of the hook, `max(1, min(cores, memory // mib_per_job))`, but not the hook's level with a memory ceiling on top.

For a package with an mib_per_job, each `--node` line now carries "memory cap N (min(current, memory_mib // mib_per_job))" right after the current clause.
The cap is not raised to 1: 0 is printed with "not one N MiB job fits", because one compile then needs more than the node has.
Flooring the bytes to MiB first does not change the quotient, so the cap is also `memory_bytes // (mib_per_job * 2**20)`; a unit test checks that identity.
Packages with no number print as before, so the bundled table, where libxc 7.0 is unmeasured, gives the same report.

The `--mib-per-job FILE` option is a visible alias of `--job-memory FILE`, the table the cap reads.
The Model Context Protocol tool keeps job_memory.
The post_ready_hook in software-layer-scripts eb_hooks.py does not divide the level EasyBuild chose.
It calls `det_parallelism()` with no arguments, which counts the cores.
Then the hook applies its PARALLELISM_LIMITS op to that count.
It keeps the result only when it is below EasyBuild's level.
EasyBuild 5.4.2 set_parallel uses `--parallel` in place of the core count when it is set, capped at `min(maxparallel, --max-parallel)`.
The `stack oom` command modelled only the bot's case, where neither is set, so the difference never showed.

The `stack oom --parallel N` and `--max-parallel N` options now give EasyBuild's level for the job: `--parallel` (which may be above the cores) or the cores, capped at max-parallel and maxparallel.
The hook op still applies to the node's cores.
On a 16-core zen4 node with `--parallel 12`, libxc gets `16 // 2 = 8`, not `12 // 2`.
With `--parallel 4`, the hook's 8 is not lower and libxc stays at 4.
The `--node` lines name the setting that decided (`then --parallel 4`, `then max-parallel 16`).
The node line names `--parallel`.
In a warning, the parallel line names `--parallel` and the cores the hook divided.
It also says when the hook's result was not lower.
A value of 0 for either flag is a usage error.
The `eb_stack_oom` tool takes parallel and max_parallel.
Without them the output is unchanged.

At software-layer-scripts 43df6f4 the hook reads EESSI_SOFTWARE_SUBDIR, the software name, EasyBuild's level and `det_parallelism()`.
No environment variable there caps PARALLELISM_LIMITS, so none is modelled.
The fixture `hooks/set-maximum-8.py` is `hooks/integers.py` with libxc's '*' row a cap of 8, `(set_maximum, 8)`, in place of `(divide_by_factor, 2)`: the form proposed in the review of software-layer-scripts pull request 337.
With it, `stack oom --hooks` gives libxc 8 on the 16-core zen4 node, the same 8 as `16 // 2`, and pull request 1772 still warns on the 32 GiB zen4 node; only the parallel line changes, to "eb_hooks.py caps libxc at 8".
A cap does not scale with the node, so the two rows differ on 64 cores (8 against 16 after max-parallel) and on 12 (8 against 6).

The fixture `hooks/divide-8.py` has a reduction of 8, `(divide_by_factor, 8)`, as asked in the review of 337 (issuecomment-6599147327): `max(1, 16 // 8)` is 2, and pull request 1772 fits on both zen4 nodes.

The how-to gets a section with the three commands.
The eessi-extend skill says to check a scripts commit with `--hooks` before a software-layer pull request pins it in `bot/software_layer_scripts_commit`.
The changelog has a Documentation entry.
The `--hooks` and `--parallel` sections of the out-of-memory how-to run `--node` before "A memory limit instead of a core fraction" says what it prints.
The `--hooks` section links to that section, and the `--parallel` section names the `CORES:MEMORY_MIB:TARGET` form before its first example.
Bot job 221273 of software-layer pull request 1772 built libxc-7.0.0-GCC-15.2.0.eb on the 32 GiB Amazon Web Services zen4 node with `bot/software_layer_scripts_commit` set to a software-layer-scripts commit that divides libxc's parallelism by 3.
Since `16 // 3` is 5, the log shows `make -j 5`, and cc1 was killed (issuecomment-6599075713).

The table in `data/build-memory.toml` did not have that job, and with 4.0 GiB per process it predicted that 5 fits (20 GiB of 32).
The observation is added, and the zen4 figure becomes the lower bound that explains it: 32 GiB over 5 is 6.4 GiB per process.
The product `5 x 6.4` is exactly 32.0 in f64, so the table still reproduces the job.
The two jobs at 8 ran out under both figures.

A libxc warning on that node now reads `8 x 6.4 GiB = 51.2 GiB` and cites software-layer-scripts 337 as well.
The maxparallel remedy is 4 in place of 7.
The why line names 8 and then 5 compilers.
The remedy line says the divider of 3 ran out too.
It names a reduction of 8, 2 cores, from the review of 337 (issuecomment-6599147327).
The review says to "really crank it" and "see if that works".
The divider of 2.5 gives 6 processes or 38.4 GiB and no longer fits, so the float examples in the how-to and the tests now also warn about memory.
The eessi-extend skill, the changelog, and the examples in the CLI reference and how-to follow.
@HaoZeke
HaoZeke merged commit 832a013 into main Nov 3, 2026
19 checks passed
@HaoZeke
HaoZeke deleted the fix/oom-hooks-memory-cap branch November 4, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant