Skip to content

fix(linux): ship the CPU torchvision, gate the bundle, repair existing installs (#778) - #823

Merged
thcp merged 5 commits into
big-releasefrom
fix/linux-cpu-torchvision-778
Oct 21, 2026
Merged

thcp merged 5 commits into
big-releasefrom
fix/linux-cpu-torchvision-778

Conversation

@thcp

@thcp thcp commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #778

What changed

  • Linux build: the CPU swap now names torchvision==0.21.0+cpu beside torch and torchaudio (new scripts/linux/bake-cpu-torch.sh, called by make-portable.sh). Both Linux variants bake CPU torch, so both get the CPU torchvision.
  • One source of truth: packaging/torch-versions.json (CPU torch line, torchvision per torch line). Read by the Linux and Windows scripts. A Rust test fails if CPU_TORCH_VERSION, CPU_TORCHVISION_VERSION or the CUDA pairs drift from it, and another fails if uv.lock stops resolving that pair. An overridden TORCH_VERSION looks up its torchvision in the same file.
  • Build gate: scripts/check_torch_bundle.py fails the build if torchvision, its nms op or onnx2torch does not load, if the pair does not match, if torchvision is a CUDA build beside non-CUDA torch, if a CPU package is not +cpu, or if a compiled torchvision file links (readelf on Linux) or ships a CUDA library. It runs after the swap and again on the stripped tree, on Linux and Windows.
  • Windows: PyPI's Windows torchvision 0.21.0 is already a CPU build (1,560,520 bytes, against 1,555,044 for +cpu and 6,140,979 for +cu124). The CPU package now names torchvision in its swap anyway, and both packages run the gate.
  • CI: new torch-bundle.yml on ubuntu-latest. It installs the project the way make-portable.sh does, checks it reproduced the cu124 torchvision beside CPU torch and that the gate rejects it, then runs bake-cpu-torch.sh, which must pass the gate. CPU torch is seeded first, so no CUDA wheels are downloaded.

How existing installs get repaired

  • New Tauri command repair_torchvision, called by setup.js on every Windows/Linux launch (fast path and full setup, after the GPU step, before warmup).
  • It reads torch/version.py and torchvision/version.py. Only torch +cpu with torchvision +cuXXX triggers pip install torchvision==<pair>+cpu --force-reinstall --no-deps from the CPU index, then an import check.
  • Healthy installs: one interpreter start for site-packages, two file reads, no network.
  • Left alone: NVIDIA installs on CUDA torch, an interrupted CUDA swap, unknown torch lines, macOS.
  • Offline: logged to setup.log, a status line for 6 s, the backend starts anyway, and the next launch tries again.
  • This also covers an NVIDIA 0.20.0 package that ended up on CPU torch (no GPU, or CUDA did not verify). It shipped the same broken pair.

What did not change

  • uv.lock and pyproject.toml: unchanged, so the runtime id is unchanged and the in-app update is still offered.
  • macOS: no script or shell code path changes. The new command returns immediately on macOS, and setup.js does not call it there.
  • The NVIDIA first-run CUDA install asks for the same specs as before. They are now built in one function with a test.

Risks

  • Rust code under cfg(not(target_os = "macos")) (the command body, verify_imports) only compiles in the Linux and Windows Rust CI jobs.
  • Not run: a real tarball build, the Windows gate (it scans .pyd import names), an NVIDIA Linux first run, or a broken 0.20.0 install being repaired.
  • The CI job's negative step depends on PyPI's torchvision 0.21.0 Linux wheel staying the cu124 build. Released files on PyPI do not change, but a version bump means updating that step.

thcp added 5 commits October 20, 2026 15:09
packaging/torch-versions.json holds the torch line the Windows and Linux
packages bake and the torchvision built for each torch line. Until now the
Linux script, the Windows script and the desktop shell each carried their own
copy, and the Linux script never named torchvision at all (#778).

scripts/check_torch_bundle.py runs with the packaged interpreter and fails
when torchvision, its nms op or onnx2torch does not load, when torchvision is
not the version paired with the installed torch, when it is a CUDA build
beside a torch that is not, when --expect-cpu is given and either is not a
+cpu build, or when a compiled torchvision file links, or torchvision.libs
ships, a CUDA library. Standard library only.
The CPU swap force-reinstalled torch and torchaudio with --no-deps and left
torchvision where the project install put it. That is PyPI's default Linux
wheel, the cu124 build, so the CPU package shipped torchvision 0.21.0+cu124
beside torch 2.6.0+cpu. Its _C.so and image.so need libcudart, libc10_cuda,
libtorch_cuda and libnvjpeg, none of which ship, so import torchvision raised
"operator torchvision::nms does not exist" and the karaoke split failed.

The swap moves to scripts/linux/bake-cpu-torch.sh, names torchvision, reads
the versions from packaging/torch-versions.json (TORCH_VERSION still
overrides, and the matching torchvision is looked up for it) and runs the
torch bundle gate. make-portable.sh runs the gate again on the stripped tree.
Both variants bake CPU torch, so both expect +cpu.
… bundle

PyPI's Windows torchvision 0.21.0 is already a CPU build (1,560,520 bytes,
against 1,555,044 for +cpu and 6,140,979 for +cu124 on the PyTorch index), so
the Windows packages were not broken. The CPU package now names torchvision in
its swap anyway, from packaging/torch-versions.json, and both packages run the
torch bundle gate so the property is checked rather than assumed.
Nothing in CI ran the swap, which is how #778 shipped. The new job installs
the project the way make-portable.sh does, checks it reproduced the CUDA
torchvision beside CPU torch and that the gate rejects it, then runs
bake-cpu-torch.sh, which must pass the gate. CPU torch is seeded first so no
CUDA torch is downloaded. Runs when the scripts, the versions file,
pyproject.toml or uv.lock change.
The in-app updater replaces the executable and backend/, never python/, and
uv.lock is unchanged, so an install that updates from 0.20.0 keeps the broken
torchvision whatever the new package ships.

repair_torchvision runs on every launch on Windows and Linux, from both the
fast path and the full setup path (after the GPU step, before warmup). It
reads torch/version.py and torchvision/version.py; only when torch is +cpu and
torchvision is +cuXXX does it force-reinstall the matching torchvision+cpu
from the CPU index and check it imports. A healthy install, an NVIDIA install
on CUDA torch, an interrupted CUDA swap and an unknown torch line are left
alone. A failed repair (offline) is logged and shown for a few seconds, never
blocks the backend, and is tried again next launch.

The decision is pure and runs on every host: version_from_version_py,
installed_version, torchvision_repair_spec, plus cuda_torch_specs, which
install_cuda_torch now uses. CPU_TORCH_VERSION, CPU_TORCHVISION_VERSION and
torchvision_version_for_tag are gated any(not(macos), test) so the tests that
pin them to packaging/torch-versions.json and uv.lock run on macOS too.

Closes #778
@thcp
thcp marked this pull request as ready for review October 21, 2026 09:37
@thcp
thcp merged commit fe1a29f into big-release Oct 21, 2026
14 checks passed
thcp added a commit that referenced this pull request Oct 22, 2026
…g-release (#836)

Closes #835

## What changed

- **PR to `main`, push to `main`, release:** full suite, as before.
- **PR to any other base:** smoke + changed specs + mapped specs. A hub
module (former `full` source entries: `main.js`, `state.js`, `mixer.js`,
`catalog.js`, `index.html`, `app/api/jobs.py`, `stems.py`,
`app/core/**`, ...) or an unmapped file under `static/` or `app/` now
adds a fixed **core set** (17 specs) instead of everything. `uv.lock`
and `pyproject.toml` add core too.
- **Full suite on a non-main PR** only for a change to how the suite
runs: `playwright.config.mjs`, `tests/e2e/helpers*`, `serve.sh`,
`seed.py`, `package*.json`, the three e2e workflows, the selector,
`spec-map.json`, or unmapped code under `tests/e2e/`.
- **New `e2e-full.yml`:** full suite on every push to `big-release`,
nightly (04:17 UTC) on `big-release`, and `workflow_dispatch` with a
`ref` input.
- **New reusable `e2e.yml`** (`workflow_call`, inputs `mode`,
`base-ref`, `checkout-ref`) holds the steps once. Same pinned SHAs,
`permissions: {}`, no secrets.
- **Concurrency:** a new push to a PR cancels that PR's older e2e run.
Pushes, releases, nightly and manual runs are never cancelled.
`timeout-minutes: 30` per shard.

## What did not change

- Application code, specs, `uv.lock`, package files. Three shards,
`--pass-with-no-tests`.
- Check names gain a prefix: `frontend-e2e / frontend-e2e (1)` to `(3)`.
A reusable workflow always prefixes the caller job. No branch protection
or ruleset requires these names today (checked).

## Real diffs, before and after

| Diff | Files | Before | After |
|---|---|---|---|
| #806 (queue.js, catalog.js) | 5 | FULL 105 | 30 (core), includes
queue-progress |
| #820 | 7 | FULL 105 | 59 (core) |
| #821 | 2 | 47 | 47 |
| #823 | 8 | 5 | 5 |
| #828 (uv.lock) | 4 | 5 | 22 (core) |
| #819, #830, #831 | 7 to 19 | FULL 105 | FULL 105 (edit
`spec-map.json`) |
| #829 | 19 | FULL 105 | FULL 105 (edits `helpers.mjs`) |
| #822 | 33 | FULL 105 | FULL 105 (edits `seed.py`) |
| `main...big-release` | 331 | FULL | FULL (PR to main) |

`npx playwright test --list <selected>` matches the selector exactly for
#806 (30 files, 200 tests) and #820 (59 files, 465 tests). Full suite:
724 tests in 105 files. Core + smoke alone: 159 tests in 22 files.

## Trade-off

A non-main PR can pass while a spec outside its core and mapped sets
breaks. The push to `big-release` run catches it on the merge commit,
minutes later.

## Limits

- **The nightly schedule only starts once `e2e-full.yml` reaches
`main`.** GitHub runs scheduled workflows from the default branch, and
the job checks out `big-release` explicitly for that reason.
- Neither concurrency nor the timeout helps a hosted runner GitHub never
assigns (queue time does not count toward the timeout). Fix by hand:
re-run the job, or `gh run cancel <id>` then `gh run rerun <id>
--failed`.
- This PR changes `ci.yml`, so its own CI runs the full suite. Expected.
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