torchcodec: cover torch 2.11 in the compatibility guard and pin per torch minor

install.sh's CUDA branch allows torch>=2.4,<2.12.0 and the cu12x/cu13x wheel
indexes top out at torch 2.11.0, so a fresh NVIDIA install lands on torch 2.11.
studio/backend/requirements/extras-no-deps.txt pinned torchcodec==0.10.0, which
upstream pairs with torch 2.10 exactly, and torchcodec publishes no
Requires-Dist: torch, so pip cannot notice. The guard that exists to catch this,
_torchcodec_version_mismatch_hint, looks the torch minor up in
_TORCH_TORCHCODEC_MINORS and returns None on a miss, and the table had no "2.11"
row: it was silent on precisely the version where the mismatch now happens.

Add the 2.11 row (mirrored into scripts/notebook_validator.py), add an
audio-torch211 extra pinning the 0.11 line, and select torchcodec at install
time from the venv's torch minor instead of a flat pin. A requirements file
cannot branch on the installed torch version, so _select_torchcodec_spec follows
the existing _select_torchao_spec pattern. It runs after the final torch repair,
which can still move torch onto another minor.

torchcodec 0.11 is the release upstream pairs with torch 2.11 exactly and the
newest one published on the cu128 index install.sh resolves torch 2.11.0 from,
since 0.12 dropped CUDA 12.8. From 0.12 torchcodec is ABI stable against torch
>= 2.11, so that half of the matrix is open ended and cannot be written as a
finite set of minors; both checkers now exempt torchcodec >= 0.12 on torch >=
2.11 rather than flagging a combination upstream supports, and torch 2.12+
installs take the open >=0.12.0 floor instead of the 2.11-only 0.11 line.

Also bump the installer step total for the new step and keep torchcodec inside
the security audit set now that it no longer comes from a requirements file.
This commit is contained in:
Daniel Han 2026-07-26 16:24:24 +00:00
commit ef240d5a61
7 changed files with 352 additions and 13 deletions

View file

@ -264,6 +264,59 @@ def _select_torchao_spec(torch_version: str | None) -> str:
return _TORCHAO_DEFAULT_SPEC
# torchcodec wheels up to 0.11 are built against exactly one torch minor and
# declare no `Requires-Dist: torch`, so pip cannot detect a mismatch and a flat
# pin in extras-no-deps.txt cannot serve every host: install.sh resolves torch
# 2.11.0 on the CUDA indexes but keeps a <2.11 default on other routes. These
# specs mirror pyproject's audio-torch2xx extras and unsloth/import_fixes.py's
# _TORCH_TORCHCODEC_MINORS (upstream's torch <-> torchcodec matrix).
#
# 0.12 onwards is ABI-stable against torch >=2.11 (torchcodec's CMakeLists sets
# TORCH_TARGET_VERSION 2.11), so torch 2.12+ takes an open-ended floor instead of
# a per-minor pin. Torch 2.11 itself keeps the 0.11 line: 0.12 dropped CUDA 12.8,
# so the cu128 index install.sh resolves torch 2.11.0 from stops at torchcodec
# 0.11.1, and a 0.12+ wheel off PyPI is built for CUDA 13.
_TORCHCODEC_DEFAULT_SPEC = "torchcodec>=0.10.0,<0.11.0"
_TORCHCODEC_ABI_STABLE_SPEC = "torchcodec>=0.12.0"
_TORCHCODEC_TORCH_SPECS: dict[int, str] = {
12: _TORCHCODEC_ABI_STABLE_SPEC,
11: "torchcodec>=0.11.0,<0.12.0",
10: "torchcodec>=0.10.0,<0.11.0",
9: "torchcodec>=0.8.0,<0.10.0",
8: "torchcodec>=0.6.0,<0.8.0",
7: "torchcodec>=0.3.0,<0.6.0",
6: "torchcodec>=0.2.0,<0.4.0",
5: "torchcodec>=0.1.0,<0.3.0",
}
_TORCHCODEC_MAX_KNOWN_MINOR = max(_TORCHCODEC_TORCH_SPECS)
def _select_torchcodec_spec(torch_version: "str | None") -> str:
"""Map an installed torch version string (e.g. '2.11.0+cu128') to the torchcodec
pip spec built against it. Falls back to _TORCHCODEC_DEFAULT_SPEC for torch <=2.4,
a non-2.x major, or an unparseable/missing version. Pure function.
"""
if not torch_version:
return _TORCHCODEC_DEFAULT_SPEC
release = str(torch_version).split("+", 1)[0] # drop +cu128/+rocm7.2/+cpu
parts = release.split(".")
try:
# Strip any pre-release/dev suffix from the minor (e.g. '11rc1' -> '11'),
# matching _select_torchao_spec.
minor_str = re.sub(r"[^0-9].*", "", parts[1]) if len(parts) > 1 else ""
major, minor = int(parts[0]), int(minor_str)
except (IndexError, ValueError):
return _TORCHCODEC_DEFAULT_SPEC
if major != 2:
return _TORCHCODEC_DEFAULT_SPEC
# Newer torch than we have a row for lands on the ABI-stable floor, which is
# forward compatible by construction (0.12+ target torch >=2.11). Never clamp
# onto the 0.11 row: that is the one release locked to torch 2.11 exactly, and
# it has no wheel on the newer CUDA indexes.
minor = min(minor, _TORCHCODEC_MAX_KNOWN_MINOR)
return _TORCHCODEC_TORCH_SPECS.get(minor, _TORCHCODEC_DEFAULT_SPEC)
def _probe_installed_torch_version() -> str | None:
"""Return torch.__version__ from the target venv (sys.executable), or None if
torch is absent/unimportable. Cross-platform (unlike probe_torch_wheel_env,
@ -2619,7 +2672,10 @@ def pip_install(
# wheel. `unsloth studio update --local` does not pass
# --no-torch, so the NO_TORCH filter above does not fire; do
# the targeted skip independently so the audio extras step
# does not take down the whole update.
# does not take down the whole update. The primary guard now
# lives on the dedicated torchcodec step, which no requirements
# file feeds; this stays as belt and braces for any file that
# reintroduces a torchcodec line.
actual_req = _filter_requirements(actual_req, {"torchcodec"})
temp_reqs.append(actual_req)
req_args_pip: list[str] = []
@ -2700,7 +2756,9 @@ def install_python_stack() -> int:
package_name = os.environ.get("STUDIO_PACKAGE_NAME", "unsloth")
# --local overlays a local repo checkout after updating deps.
local_repo = os.environ.get("STUDIO_LOCAL_REPO", "")
base_total = 11 if IS_WINDOWS else 12 # +1 for the anyio repair check (step 8b)
# +1 for the anyio repair check (step 8b), +1 for torchcodec (step 13b, which
# reports progress on every branch including its skips).
base_total = 12 if IS_WINDOWS else 13
if IS_MACOS:
base_total -= 1 # triton step is skipped on macOS
if not IS_MACOS and not NO_TORCH:
@ -3072,6 +3130,31 @@ def install_python_stack() -> int:
_ensure_rocm_torch()
_ensure_cpu_torch()
# 13b. torchcodec -- pinned to the line built against the venv's torch (see
# _select_torchcodec_spec), so it must run *after* the repair above:
# that repair can move torch onto another minor (cu128 resolves 2.11.0),
# and a torchcodec picked before it would be stale again. It cannot live
# in extras-no-deps.txt because pip environment markers cannot branch on
# the installed torch version. Skipped on the same platforms pip_install
# filtered it out for (no torch at all, or no published wheel).
if NO_TORCH:
_progress("torchcodec (skipped, no torch)")
elif PLATFORM_LACKS_TORCHCODEC_WHEEL:
_progress("torchcodec (skipped, no wheel for this platform)")
else:
_progress("torchcodec")
_codec_torch_ver = _probe_installed_torch_version()
_codec_spec = _select_torchcodec_spec(_codec_torch_ver)
_safe_print(
f" torch {_codec_torch_ver or 'unknown'} detected -- installing {_codec_spec}"
)
pip_install(
"Installing torchcodec",
"--no-deps",
"--no-cache-dir",
_codec_spec,
)
# 14. Final check (silent; third-party conflicts are expected)
subprocess.run(
[sys.executable, "-m", "pip", "check"],