Repository navigation
fix(build): keep torch's dependency tree out of tarballs - #390
Conversation
There was a problem hiding this comment.
🟡 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.
d3cc1c7 to
7a951f1
Compare
OneAsterisk
left a comment
There was a problem hiding this comment.
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_buildnow 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_namereturns""for archive filenames, so"" in rootsskips pruning. #390 (comment) - copilot on
build.py(--excludedropping overlapping torch deps): Verified fixed. Non-image exclusions joinroots(build.py:1268), andtest_tarball_keeps_dependencies_of_user_excluded_torch_dependencycovers it. #390 (comment) - @deanq on
build.py:463(wheel filename plus marker): Verified fixed at d3cc1c7. A;after a.whl/.tar.gzname now returns"". #390 (comment) - @deanq on
build.py(_canonical_namevs_normalize_package_name): Verified fixed at d3cc1c7._normalize_package_nameis gone andSIZE_PROHIBITIVE_CANONICALis module-level. One leftover duplicate set remains increate_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)" | 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_normalizedandexcluded_canonicalare 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=Noneprunes with no roots —build.py:1213. WithNone,rootsis empty and"" not in rootsholds. Pruning then runs and drops a user's direct dependency whenever torch also needs it (for examplenumpybeside torchvision). The only caller passes the list today, so nothing breaks now. Skipping pruning when the argument isNonewould match the PR's fail-closed rule for future callers.
Nits
build.py:457torch_familyonly aliasesSIZE_PROHIBITIVE_CANONICAL. Also,_requirement_name(req)runs twice per requirement (463,470).build.py:470A range such astorch>=2.0triggers "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
- Merge the two exclusion sets in
create_tarball(about 3 lines removed). This can go in this PR.
Verified clean
- Graph closure: the
seed/stoplogic 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-flashinrequirements.txt):_remove_runpod_flash_from_requirementsdeletes that dist-info beforecreate_tarballruns. 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. RECORDpath normalization drops../script paths (they stay in the archive, which is safe).- Wheel and URL parsing for
#sha256,?query,%2Blocal versions, and markers. Requirement-name regex onname @ 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/fsspecon 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.
OneAsterisk
left a comment
There was a problem hiding this comment.
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 | #113 is open with review required. Latest worker release is v1.6.0 (2026-08-20). |
Findings
None.
Verified clean
- Renaming
excluded_canonicaltoexcluded_normalizedchanges 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 theNoneguard. - The new
Nonetest keepsnumpy,nvidiaandPIL. The previous head would have pruned them, so the test can fail. - Regex edge cases:
torch == 2.0andtorch===2.0still 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.
…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.
7a6e78a to
9d3e1f4
Compare

Warning
Do not merge until runpod-workers/flash#113 is merged and released. This PR keeps
torchvisionout of the tarball and tells users the GPU worker image provides it. The GPU load-balancer image doesn't shiptorchvisionuntil runpod-workers/flash#113 lands, so load-balanced GPU apps that import it would fail after this merges.Stops
flash buildfrom bundling the wheels that onlytorchandtorchvisionneed, 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 itstorch 2.9.1. The tarball is extracted ahead of site-packages, so a bundled PyPItorchvision(0.29.1, needstorch>=2.14) would override it and fail to load.torchvisionstays excluded. The load-balanced image is missing it, and that's fixed in runpod-workers/flash#113.What was actually breaking
torch(timm,accelerate,sentence-transformers) installs about 2 GB ofnvidia-*/cuda-*wheels. Only the exact namestorch/torchvision/... were stripped. An app whose only dependency wastimmbuilt to 1957.5 MB and failed the 1500 MB limit.functorch/,torchgen/andtorchvision.libs/leaked into every archive that pulled intorch.Change
Requires-Distgraph), plus every file theirRECORDlists unless a kept wheel installed it too. Files noRECORDlists stay.METADATA/RECORD, aRequires-Distline is unreadable, or a requirement line has no name (bare URL, path, archive filename,-r). Your own requirements and--excludedependencies are never pruned.not bundling torchvision: provided by the Runpod GPU worker image, and warn when a pin liketorchvision==0.29.1,~=0.24.1,@ urlor adownload.pytorch.orgwheel URL is ignored.--excludenow also matches~=and@ urlrequirements.timmappVerification
flash buildof a one-dependencytimmapp in alinux/amd64python:3.12-slimcontainer, before and after: 1957.5 MB failing, then 32.5 MB with nothing from torch/nvidia/cuda/triton left in the archive.sympy,mpmath,networkx,functorch,torchgendropped).-n auto -m "not serial") + 53 serial, coverage 86.82%.ruff checkandruff format --checkclean. No new mypy errors inbuild.py.nvidia/dirs, missingRECORD/METADATA, unreadableRequires-Dist, direct-URL, bare-URL and archive-filename requirements,~=/@pins,RECORDpath normalization, unrecorded files,--excludedependencies (including onestorchalso needs), and the notices. Each review fix's test was confirmed to fail on the earlier code.create_tarball, pruning skipped whenroot_requirementsisNone, and the ignored-pin warning limited to==,~=,@and wheel paths, sotorch>=2.0no longer warns. The 3 new cases fail on the previous head. Full unit suite: 2697 passed + 43 serial, coverage 86.03%.torch/is dropped whole, and nested-r other.txtfiles go to pip unparsed (pruning is skipped for them).Open questions from review
sympy,networkx,jinja2andfsspecfor 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
torchvisionin the GPU load-balancer imageflash buildarchivesrunpod-flash1.21.1 publishes.flash deployrunpod-flash1.21.1. The release commit is on main, but the Release workflow failed, so it isn't tagged or on PyPI yet.apikeyas a credentials fallback