Skip to content

[QDP] Refuse .npy loads that would exceed host memory (#787) - #1554

Draft
andrewmusselman wants to merge 3 commits into
mainfrom
pr/787-npy-size-check
Draft

andrewmusselman wants to merge 3 commits into
mainfrom
pr/787-npy-size-check

Conversation

@andrewmusselman

Copy link
Copy Markdown
Contributor

Purpose of PR

NumpyReader read the whole array before looking at its size, so a large or corrupt .npy file could OOM-kill the process. Following @rich7420's design comment on #787, the reader now parses only the header, estimates the in-memory size, and refuses the load before reading any data when it would not fit. The limit is automatic by default and configurable.

Related Issues or PRs

Closes #787 (follow-up from the #777 review; claimed by @ryankert01 in the thread)

Changes Made

  • qdp-core/src/readers/numpy.rs:
    • header-only parse (uses py_literal, already in the tree via ndarray-npy); prod(shape) * itemsize with checked arithmetic;
    • pure check_npy_memory(header, limit, available);
    • Auto: refuse when estimate * 2 > 0.8 * MemAvailable; unknown memory allows the load with a warning;
    • QDP_NPY_MAX_BYTES=<bytes|16G|512MiB> or off; invalid values are errors;
    • NumpyReader::with_memory_limit();
    • file length checked against the header so truncated files fail before allocation, even with the guard off;
    • error names the estimate, the limit, how to override, and suggests Parquet streaming.
  • New qdp-core/src/platform/host_memory.rs: available_host_memory_bytes() (/proc/meminfo on Linux, None elsewhere).
  • Tests: crafted headers claiming huge shapes (e.g. (10^12, 10) float64) in tests/numpy.rs; env-var tests in their own binary tests/numpy_env.rs.
  • Docs: docs/qdp/readers.md section and a troubleshooting row in docs/qdp/getting-started.md.

Testing

  • cargo test -p qdp-core --test numpy --test numpy_env: 11 + 1 passed (main: 4 in numpy.rs).
  • cargo test -p qdp-core --lib: 112 passed, 3 failed (main 97/3; same 3 GPU tests; 15 new unit tests).
  • Full cargo test -p qdp-core --no-fail-fast: 215 passed, 145 failed (main 192 passed; the failures are the same no-GPU set).
  • cargo fmt, clippy -D warnings, cargo check -p qdp-python --all-targets: clean.
  • Not verified: Python tests on a GPU and the real error text through PyO3; macOS/Windows (warn-and-allow path is unit-tested only); clippy --all-features (needs libtorch). In containers, MemAvailable is the host's figure, not the cgroup limit.

Merging

No textual conflicts, but if the #723 PR merges first, two of its new tests in qdp-core/tests/numpy_edge_cases.rs (truncated and non-NPY files) expect the old parser's wording. This PR's size pre-check rejects both files earlier with clearer messages. The after-723/ version adds one commit updating those two expectations; both files are still rejected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GuXmXRXdNF43h2FoQcNAZb

andrewmusselman and others added 3 commits October 20, 2026 16:39
NumpyReader read the whole array before looking at its size, so a
large (or corrupt) .npy file could OOM-kill the process. It now
parses only the header first, estimates prod(shape) * itemsize with
checked arithmetic, and applies a limit before any data is read:

- Auto (default): refuse when estimate * 2 > 0.8 * MemAvailable, as
  proposed in the PR #777 review. If available memory is unknown the
  load is allowed and a warning is logged (previous behaviour).
- QDP_NPY_MAX_BYTES=<bytes|16G|512MiB>: explicit cap on the estimate.
- QDP_NPY_MAX_BYTES=off: disable. Invalid values are errors.
- NumpyReader::with_memory_limit() sets the policy without touching
  the environment.

The decision is a pure function, check_npy_memory(header, limit,
available), so it is tested without allocating. The error names the
estimate, the limit, how to override it, and suggests Parquet
streaming. A second check compares the file length to the header so
truncated files fail before allocation, even with the guard off.

MemAvailable comes from a new platform::available_host_memory_bytes()
(/proc/meminfo on Linux, None elsewhere). Inside a container that is
the host's figure, not the cgroup limit; reviewers may want a
cgroup-aware follow-up.

The header parser is ours rather than ndarray-npy's because that
crate keeps its header module private; it uses py_literal, which
ndarray-npy already depends on, so no new crate enters the tree.

Part of #787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integration tests write tiny .npy files whose header claims a huge
shape, e.g. (10^12, 10) float64 (~72.76 TiB), with no body. They
check that the reader fails fast on the size check (explicit and
auto limits), on the truncation check (guard disabled), and on
shape overflow, and that small C- and Fortran-order files still
load under a tight limit.

QDP_NPY_MAX_BYTES tests mutate the process environment, so they live
in their own test binary (tests/numpy_env.rs) with a single #[test]
function; no other thread can observe a half-set variable.

Part of #787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the header-based size check on the Readers page: the auto
formula, accepted QDP_NPY_MAX_BYTES values, behaviour when
MemAvailable is unknown or containerised, the truncation check, and
the Parquet streaming alternative. Add a troubleshooting row to
Getting Started for the new error message.

Closes #787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Hackathon Community Over Code Hackathon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QDP][followup] Numpy file size check to prevent OOM

1 participant