Skip to content

fix(build): keep torch's dependency tree out of tarballs - #390

Open
justinwlin wants to merge 4 commits into
mainfrom
fix/torch-dependency-tree
Open

justinwlin wants to merge 4 commits into
mainfrom
fix/torch-dependency-tree

Conversation

@justinwlin

@justinwlin justinwlin commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Stops flash build from bundling the wheels that only torch and torchvision need, and tells users what was left out. Supersedes #376. Refs #353.

Why not un-exclude torchvision (#376)

The GPU worker image installs torchvision==0.24.1, paired with its torch 2.9.1. The tarball is extracted ahead of site-packages, so a bundled PyPI torchvision (0.29.1, needs torch>=2.14) would override it and fail to load. torchvision stays excluded. The load-balanced image is missing it, and that's fixed in runpod-workers/flash#113.

What was actually breaking

  • On Linux x86_64, any requirement that depends on torch (timm, accelerate, sentence-transformers) installs about 2 GB of nvidia-*/cuda-* wheels. Only the exact names torch/torchvision/... were stripped. An app whose only dependency was timm built to 1957.5 MB and failed the 1500 MB limit.
  • functorch/, torchgen/ and torchvision.libs/ leaked into every archive that pulled in torch.
  • Exclusions were logged at debug level only, and version pins were silently ignored.

Change

  • Drop wheels that only the torch family depends on (Requires-Dist graph), plus every file their RECORD lists unless a kept wheel installed it too. Files no RECORD lists stay.
  • Fail closed: no pruning when a wheel lacks METADATA/RECORD, a Requires-Dist line is unreadable, or a requirement line has no name (bare URL, path, archive filename, -r). Your own requirements and --exclude dependencies are never pruned.
  • Print not bundling torchvision: provided by the Runpod GPU worker image, and warn when a pin like torchvision==0.29.1, ~=0.24.1, @ url or a download.pytorch.org wheel URL is ignored.
  • --exclude now also matches ~= and @ url requirements.
Same timm app Before After
Linux x86_64 build 1957.5 MB, fails 32.5 MB
macOS build 88 MB 79 MB
Verification
  • Real flash build of a one-dependency timm app in a linux/amd64 python:3.12-slim container, before and after: 1957.5 MB failing, then 32.5 MB with nothing from torch/nvidia/cuda/triton left in the archive.
  • The same app on macOS, where pip evaluates dependency markers against the host and skips the nvidia wheels: 88 MB, then 79 MB (sympy, mpmath, networkx, functorch, torchgen dropped).
  • CI split locally: 2726 passed (-n auto -m "not serial") + 53 serial, coverage 86.82%. ruff check and ruff format --check clean. No new mypy errors in build.py.
  • 17 new unit tests (30 cases) cover pruning, shared nvidia/ dirs, missing RECORD/METADATA, unreadable Requires-Dist, direct-URL, bare-URL and archive-filename requirements, ~=/@ pins, RECORD path normalization, unrecorded files, --exclude dependencies (including ones torch also needs), and the notices. Each review fix's test was confirmed to fail on the earlier code.
  • Reviewed by Copilot and six Codex passes, with the last pass reporting no remaining defects. Left as on main: a project directory named torch/ is dropped whole, and nested -r other.txt files go to pip unparsed (pruning is skipped for them).

…exclusions

torch-family packages stay excluded: the GPU worker image ships torch 2.9.1
and a torchvision pinned to it, and the tarball sits ahead of site-packages,
so a bundled PyPI torchvision (0.29.1 needs torch>=2.14) would shadow the
paired one. But pip still installs their whole dependency tree, and only
exact package names were stripped:

- On Linux x86_64, any requirement that depends on torch (timm, accelerate,
  sentence-transformers) pulls ~2 GB of nvidia/cuda wheels. A one-dependency
  timm app built to 1957.5 MB and failed the 1500 MB limit.
- functorch/, torchgen/ and torchvision.libs/ leaked into the archive.

Drop wheels that only the torch family depends on (from Requires-Dist), and
every file their RECORD lists unless a kept wheel also installed it. Files no
RECORD lists stay. Pruning is skipped when any wheel lacks METADATA or
RECORD, or a requirement line has no readable name (bare URL, path, -r).
A user --exclude still drops only the named package. The same timm app now
builds to 32.5 MB.

Builds also print which torch-family requirements were not bundled, and
warn when a version pin on them is ignored, instead of a debug-only log.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pruning can remove explicitly requested wheels and dependencies of user-excluded packages, while some ignored pins still bypass warnings.

3 open findings
What changed in this PR

Reduces flash build archive sizes by pruning dependencies needed only by excluded torch-family packages.

Changes:

  • Adds dependency-graph pruning and file ownership filtering.
  • Displays exclusion notices and ignored-version warnings.
  • Adds tests for pruning safeguards and notices.
File Description
tests/​unit/​cli/​commands/​test_build.py Tests pruning, file preservation, and notices.
src/​runpod_flash/​cli/​commands/​build.py Implements dependency pruning, archive filtering, and notices.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/runpod_flash/cli/commands/build.py Outdated
Comment thread src/runpod_flash/cli/commands/build.py Outdated
Comment thread src/runpod_flash/cli/commands/build.py
- Detect torch-family requirements with _requirement_name, so "~=" pins and
  "name @ url" forms are excluded and warned about instead of reaching pip.
- Treat bare archive filenames (*.whl, *.tar.gz) as unnamed, so pruning
  fails closed instead of dropping the wheel they install.
- Protect dependencies of user --exclude packages even when they overlap
  torch's tree, so --exclude removes only the named package.
… or @

- Name wheel paths and URLs by their filename, so a pinned
  download.pytorch.org torch/torchvision wheel is excluded and warned about
  instead of reaching pip.
- extract_package_name now splits on ~, @, whitespace and (, so
  --exclude sympy matches "sympy~=1.14" and "sympy @ url".
- Only treat a requirement as an archive filename when the whole line is
  one, so a distribution named "my.whl" still counts as a root.
if should_exclude_package(req, excluded_packages):
# _requirement_name also reads "name @ url" forms, and wheel
# paths or URLs are named by their filename.
family_name = _requirement_name(req) or _wheel_distribution_name(req)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to the resolved bare-wheel thread: with a marker, _requirement_name("torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl; platform_system=='Linux'") returns torch_2_9_1_cp312_..._whl (the regex accepts the whole filename because ; follows it), so the or _wheel_distribution_name(req) fallback never runs. The torch wheel isn't recognized, isn't filtered, and gets no ignored-pin warning, so pip installs the ~2.5 GB torch + CUDA tree.

Checking _wheel_distribution_name(req) first (_wheel_distribution_name(req) or _requirement_name(req)), or having _requirement_name return "" when the name ends in .whl regardless of what follows, covers it.

return False


def _canonical_name(name: str) -> str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_canonical_name duplicates _normalize_package_name (line 261), and they disagree on .: _is_excluded_top_dir checks package dirs with _normalize_package_name and dist-info dirs with _canonical_name, so a name containing . is normalized two different ways within the same function. Since _canonical_name is the PEP 503 one, replacing _normalize_package_name with it keeps a single rule.

Related: {_canonical_name(p) for p in SIZE_PROHIBITIVE_PACKAGES} is rebuilt in run_build and twice in create_tarball. A module-level frozenset next to SIZE_PROHIBITIVE_PACKAGES keeps the requirement filter and the tarball pruning on the same definition.

This branch has not been deployed

No deployments
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.

3 participants