Skip to content

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

Merged
justinwlin merged 7 commits into
mainfrom
fix/torch-dependency-tree
Oct 29, 2026
Merged

justinwlin merged 7 commits into
mainfrom
fix/torch-dependency-tree

Conversation

@justinwlin

@justinwlin justinwlin commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Do not merge until runpod-workers/flash#113 is merged and released. This PR keeps torchvision out of the tarball and tells users the GPU worker image provides it. The GPU load-balancer image doesn't ship torchvision until runpod-workers/flash#113 lands, so load-balanced GPU apps that import it would fail after this merges.

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.
  • 20 new unit tests (47 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.
  • Follow-up to the approving review (7a6e78a): one canonical exclusion set in create_tarball, pruning skipped when root_requirements is None, and the ignored-pin warning limited to ==, ~=, @ and wheel paths, so torch>=2.0 no longer warns. The 3 new cases fail on the previous head. Full unit suite: 2697 passed + 43 serial, coverage 86.03%.
  • 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).

Open questions from review

  • Load-balancer image and torchvision: not shipped yet. fix(docker): install torchvision in GPU load-balancer image runpod-workers/flash#113 is approved but not merged, and the latest worker release is v1.6.0 (2026-08-20). That's the reason for the merge hold above.
  • CPU workers in mixed apps: GPU and CPU resources share one tarball, so a torch-dependent requirement also drops sympy, networkx, jinja2 and fsspec for CPU workers. torch itself was already excluded there, so code that needs those packages through torch already couldn't import torch on CPU workers. This is intended.

Related Flash PRs

PR What it does Status (2026-10-29) Remaining
runpod-workers/flash#113 Installs torchvision in the GPU load-balancer image Merged 2026-10-27, not released Ships with release PR runpod-workers/flash#112 (1.6.1), still open.
#390 Keeps torch's dependency tree out of flash build archives Merged 2026-10-29 Needs the 1.6.1 worker image live before runpod-flash 1.21.1 publishes.
#394 Waits for cold-start jobs instead of failing at 90s, and prints a working curl example after flash deploy Merged 2026-10-29 Ships in runpod-flash 1.21.1. The release commit is on main, but the Release workflow failed, so it isn't tagged or on PyPI yet.
runpod/docs#923 Matches the Flash quickstart and tutorials to a live run on 1.21.0 Merged 2026-10-29 Revisit the 90-second and curl guidance once 1.21.1 is on PyPI.
#369 Reads runpodctl's top-level apikey as a credentials fallback Closed 2026-10-29 Superseded by #363 and #382.

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 Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved

@OneAsterisk OneAsterisk 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.

Claude review, round 1

Verdict: Approved. The pruning logic fails closed where it should, and every prior thread is fixed at head.
Approval gate: 0 Critical, 0 High, 2 Medium+Low → approved.

Existing feedback

  • copilot on build.py (ignored-pin bypass for ~=/@): Verified fixed. run_build now names requirements with _requirement_name(req) or _wheel_distribution_name(req) (build.py:463). #390 (comment)
  • copilot on build.py (bare wheel filename misread as a name): Verified fixed. _requirement_name returns "" for archive filenames, so "" in roots skips pruning. #390 (comment)
  • copilot on build.py (--exclude dropping overlapping torch deps): Verified fixed. Non-image exclusions join roots (build.py:1268), and test_tarball_keeps_dependencies_of_user_excluded_torch_dependency covers it. #390 (comment)
  • @deanq on build.py:463 (wheel filename plus marker): Verified fixed at d3cc1c7. A ; after a .whl/.tar.gz name now returns "". #390 (comment)
  • @deanq on build.py (_canonical_name vs _normalize_package_name): Verified fixed at d3cc1c7. _normalize_package_name is gone and SIZE_PROHIBITIVE_CANONICAL is module-level. One leftover duplicate set remains in create_tarball (Low below). #390 (comment)

PR description vs code

Claim Status Where
Drop wheels only the torch family depends on, via Requires-Dist ✅ _excluded_only_dists, build.py:1149
Drop files their RECORD lists unless a kept wheel installed them; unrecorded files stay ✅ build.py:1302-1308, 1331-1340
Fail closed on missing METADATA/RECORD, unreadable Requires-Dist, unnamed requirement ✅ build.py:1177-1188, 1274
--exclude dependencies never pruned ✅ build.py:1268
Visible "not bundling" notice and ignored-pin warning for ==, ~=, @ url, wheel URLs ✅ build.py:482-492
--exclude matches ~= and @ url ✅ extract_package_name, build.py:819
"17 new unit tests (30 cases)" ⚠️ stale Head adds 21 test functions, 44 cases (later commits added more).
Size numbers (1957.5 MB → 32.5 MB, 88 → 79 MB) Not verified Needs a real build.

Findings

Low

  • excluded_normalized and excluded_canonical are the same set — build.py:1233, build.py:1258. Both are {_canonical_name(p) for p in excluded_packages}. Keep one, so the tar filter and the pruning step cannot drift apart. This finishes deanq's single-rule cleanup.
  • root_requirements=None prunes with no roots — build.py:1213. With None, roots is empty and "" not in roots holds. Pruning then runs and drops a user's direct dependency whenever torch also needs it (for example numpy beside torchvision). The only caller passes the list today, so nothing breaks now. Skipping pruning when the argument is None would match the PR's fail-closed rule for future callers.

Nits

  • build.py:457 torch_family only aliases SIZE_PROHIBITIVE_CANONICAL. Also, _requirement_name(req) runs twice per requirement (463, 470).
  • build.py:470 A range such as torch>=2.0 triggers "Version pins ignored" even though the image satisfies it. Consider warning only on ==, ~=, @, and wheel paths.
  • PR body test count is stale (see table).

Simplification opportunities

  1. Merge the two exclusion sets in create_tarball (about 3 lines removed). This can go in this PR.

Verified clean

  • Graph closure: the seed/stop logic keeps any wheel that a root or a non-torch-pulled wheel reaches. Ignoring markers and extras only adds edges, so it can only keep more.
  • Default skeleton (runpod-flash in requirements.txt): _remove_runpod_flash_from_requirements deletes that dist-info before create_tarball runs. Its deps (runpod, pydantic, ...) are not torch-pulled, so they stay as seeds.
  • Tar walk: shared nvidia/ filtered file by file. Symlinked dirs are not followed and are added as links. Top-level files owned by dropped wheels are removed by _is_excluded_top_dir.
  • RECORD path normalization drops ../ script paths (they stay in the archive, which is safe).
  • Wheel and URL parsing for #sha256, ?query, %2B local versions, and markers. Requirement-name regex on name @ url, extras, and old-style parens.
  • Tests use fake dist-info trees and patch install_dependencies. No network access and no real pip runs.
  • CI: all checks pass at 7a951f1 ("Build Package" skipped by design).

Questions for the author

  • The notice says torchvision is "provided by the Runpod GPU worker image". The body says the load-balanced image lacks it until runpod-workers/flash#113. Has that shipped? If not, LB GPU apps will see this notice and then fail to import torchvision.
  • GPU and CPU resources share one tarball. Any torch-dependent requirement now also loses sympy/networkx/jinja2/fsspec on CPU workers. torch was already missing there, so this is likely moot. Is it intended?

Reviewed by Claude (claude-opus-5-5) via the pr-review skill. Tokens: 30 in / 20,128 out / 1,005,924 cache read / 86,997 cache write = 1,113,079. Estimated cost: $3.00 from the skill's bundled sum_usage.py rate card, assuming 5-minute cache writes. Round 1 of this PR. This is automated assistance; a human owns the final call.

Comment thread src/runpod_flash/cli/commands/build.py Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved
Comment thread src/runpod_flash/cli/commands/build.py Resolved

@OneAsterisk OneAsterisk 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.

Claude review, round 2

Verdict: Approved. 7a6e78a fixes both round 1 Lows and the range-pin nit, with no new issues.
Approval gate: 0 Critical, 0 High, 0 Medium+Low → approved. Quality Gates and Nix flake were still pending when this was posted. Pre-check and Guard pass.

Status of prior findings

# Finding Severity Status Evidence
R1-1 excluded_normalized and excluded_canonical are the same set Low Fixed build.py:1258-1278 now uses only excluded_normalized, 7a6e78a
R1-2 root_requirements=None prunes with no roots Low Fixed build.py:1271 guards on root_requirements is not None. Docstring updated. test_tarball_skips_pruning_without_root_requirements covers it, 7a6e78a
R1-3 torch_family alias, _requirement_name computed twice Nit Fixed Alias removed. req_name computed once at build.py:461
R1-4 torch>=2.0 warns "Version pins ignored" Nit Fixed build.py:472 matches ==|~=|@ only. test_auto_exclude_does_not_warn_on_version_ranges covers 2 range forms
R1-5 PR body test count stale Nit Fixed Body now says 20 tests (47 cases), which matches the diff

Existing feedback

  • copilot (3 threads) and @deanq (2 threads): all resolved and verified fixed in round 1. Nothing new.

PR description vs code (changes since round 1)

Claim Status Where
One canonical exclusion set in create_tarball ✅ build.py:1230-1278
Pruning skipped when root_requirements is None ✅ build.py:1271
Ignored-pin warning limited to ==, ~=, @ and wheel paths ✅ build.py:472
Merge hold until runpod-workers/flash#113 ships ⚠️ still applies #113 is open with review required. Latest worker release is v1.6.0 (2026-08-20).

Findings

None.

Verified clean

  • Renaming excluded_canonical to excluded_normalized changes no behavior. Both were {_canonical_name(p) for p in excluded_packages}. The only caller (build.py:547) passes a list, so production behavior is unchanged by the None guard.
  • The new None test keeps numpy, nvidia and PIL. The previous head would have pruned them, so the test can fail.
  • Regex edge cases: torch == 2.0 and torch===2.0 still warn. !=, <=, >= do not. Markers are split off before matching, so @ in a marker cannot trigger it.

Questions for the author

  • Trade-off of the range change: a lower bound above the image's version (torch>=2.14, image has 2.9.1) is now dropped without a warning. Is that acceptable, or worth a follow-up that compares against the image's versions?

Reviewed by Claude (claude-opus-5-5) via the pr-review skill. Tokens: 16 in / 3,614 out / 404,743 cache read / 53,152 cache write = 461,525 (measured before writing this body). Estimated cost: $1.35 from the skill's bundled sum_usage.py rate card, assuming 5-minute cache writes. Round 2 of this PR. This is automated assistance; a human owns the final call.

justinwlin and others added 6 commits October 27, 2026 10:19
…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.
- 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.
…name normalization

_requirement_name now returns "" for an archive filename followed by an
environment marker, so the _wheel_distribution_name fallback runs and a
torch wheel with a marker is filtered and warned about.

Replace _normalize_package_name with the PEP 503 _canonical_name so
tarball filtering uses a single normalization rule, and hoist the
canonical torch-family set into SIZE_PROHIBITIVE_CANONICAL.
…e pins

- create_tarball kept two copies of the canonical exclusion set. Use
  one, so the tar filter and the pruning step cannot drift apart.
- Skip pruning when root_requirements is None. With no roots nothing is
  protected, so a user's direct dependency that torch also needs would
  be dropped. Only the excluded packages themselves are removed.
- Warn about ignored pins only for ==, ~=, @ and wheel paths. A range
  such as torch>=2.0 is usually satisfied by the image's version.
- Compute the requirement name once per requirement and drop the
  torch_family alias.
@justinwlin
justinwlin merged commit 98ab4cf into main Oct 29, 2026
10 checks passed
@justinwlin
justinwlin deleted the fix/torch-dependency-tree branch October 29, 2026 13:27
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.

4 participants